timeshift variable, handle nested isr+api, bump max_interrupts
This commit is contained in:
parent
c7bf1be8b1
commit
b9e388d9d5
@ -40,6 +40,16 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
pub static mut FUZZ_START_TIMESTAMP : SystemTime = UNIX_EPOCH;
|
pub static mut FUZZ_START_TIMESTAMP : SystemTime = UNIX_EPOCH;
|
||||||
|
|
||||||
|
pub const QEMU_ICOUNT_SHIFT : u32 = 5;
|
||||||
|
pub const QEMU_ISNS_PER_SEC : u32 = u32::pow(10, 9) / u32::pow(2, QEMU_ICOUNT_SHIFT);
|
||||||
|
pub const QEMU_ISNS_PER_USEC : u32 = QEMU_ISNS_PER_SEC / 1000000;
|
||||||
|
pub const QEMU_NS_PER_ISN : u32 = 1 << QEMU_ICOUNT_SHIFT;
|
||||||
|
pub const TARGET_SYSCLK_FREQ : u32 = 25 * 1000 * 1000;
|
||||||
|
pub const TARGET_MHZ_PER_MIPS : f32 = TARGET_SYSCLK_FREQ as f32 / QEMU_ISNS_PER_SEC as f32;
|
||||||
|
pub const TARGET_MIPS_PER_MHZ : f32 = QEMU_ISNS_PER_SEC as f32 / TARGET_SYSCLK_FREQ as f32;
|
||||||
|
pub const TARGET_SYSCLK_PER_QEMU_SEC : u32 = (TARGET_SYSCLK_FREQ as f32 * TARGET_MIPS_PER_MHZ) as u32;
|
||||||
|
pub const QEMU_SYSCLK_PER_TARGET_SEC : u32 = (TARGET_SYSCLK_FREQ as f32 * TARGET_MHZ_PER_MIPS) as u32;
|
||||||
|
|
||||||
//========== Metadata
|
//========== Metadata
|
||||||
#[derive(Debug, SerdeAny, Serialize, Deserialize)]
|
#[derive(Debug, SerdeAny, Serialize, Deserialize)]
|
||||||
pub struct QemuIcountMetadata {
|
pub struct QemuIcountMetadata {
|
||||||
@ -222,7 +232,7 @@ where
|
|||||||
{
|
{
|
||||||
// TODO Replace with match_name_type when stable
|
// TODO Replace with match_name_type when stable
|
||||||
let observer = observers.match_name::<QemuClockObserver>(self.name()).unwrap();
|
let observer = observers.match_name::<QemuClockObserver>(self.name()).unwrap();
|
||||||
self.exec_time = Some(Duration::from_nanos(observer.last_runtime() << 4)); // Assume a somewhat realistic multiplier of clock, it does not matter
|
self.exec_time = Some(Duration::from_nanos(observer.last_runtime() << QEMU_ICOUNT_SHIFT)); // Assume a somewhat realistic multiplier of clock, it does not matter
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,17 @@ use core::time::Duration;
|
|||||||
use std::{env, path::PathBuf, process::{self, abort}, io::{Read, Write}, fs::{self, OpenOptions}, cmp::{min, max}, mem::transmute_copy, collections::btree_map::Range, ptr::addr_of_mut, ffi::OsStr};
|
use std::{env, path::PathBuf, process::{self, abort}, io::{Read, Write}, fs::{self, OpenOptions}, cmp::{min, max}, mem::transmute_copy, collections::btree_map::Range, ptr::addr_of_mut, ffi::OsStr};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use libafl_bolts::{
|
use libafl_bolts::{
|
||||||
core_affinity::Cores, current_nanos, rands::StdRand, shmem::{ShMemProvider, StdShMemProvider}, tuples::tuple_list, AsMutSlice, AsSlice
|
core_affinity::Cores, current_nanos, rands::StdRand, shmem::{ShMemProvider, StdShMemProvider}, tuples::tuple_list, AsMutSlice, AsSlice
|
||||||
};
|
};
|
||||||
use libafl::{
|
use libafl::{
|
||||||
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, events::{launcher::Launcher, EventConfig}, executors::{ExitKind, TimeoutExecutor}, feedback_or, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, fuzzer::{Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, monitors::MultiMonitor, observers::VariableMapObserver, prelude::{havoc_mutations, minimizer::TopRatedsMetadata, CorpusId, Generator, HasBytesVec, HitcountsMapObserver, RandBytesGenerator, SimpleEventManager, SimpleMonitor, SimpleRestartingEventManager, StdScheduledMutator}, schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, stages::StdMutationalStage, state::{HasCorpus, HasMetadata, HasNamedMetadata, StdState}, Error, Evaluator
|
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, events::{launcher::Launcher, EventConfig}, executors::{ExitKind, TimeoutExecutor}, feedback_or, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, fuzzer::{Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, monitors::MultiMonitor, observers::VariableMapObserver, prelude::{havoc_mutations, minimizer::TopRatedsMetadata, CorpusId, Generator, HasBytesVec, HitcountsMapObserver, RandBytesGenerator, SimpleEventManager, SimpleMonitor, SimpleRestartingEventManager, StdScheduledMutator}, schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, stages::StdMutationalStage, state::{HasCorpus, HasMetadata, HasNamedMetadata, StdState}, Error, Evaluator
|
||||||
};
|
};
|
||||||
use libafl_qemu::{
|
use libafl_qemu::{
|
||||||
edges::{self, edges_map_mut_slice, QemuEdgeCoverageHelper, MAX_EDGES_NUM}, elf::EasyElf, emu::{libafl_qemu_remove_native_breakpoint, libafl_qemu_set_native_breakpoint, Emulator}, GuestAddr, GuestPhysAddr, QemuExecutor, QemuHooks, QemuInstrumentationFilter, Regs
|
edges::{self, edges_map_mut_slice, QemuEdgeCoverageHelper, MAX_EDGES_NUM}, elf::EasyElf, emu::{libafl_qemu_remove_native_breakpoint, libafl_qemu_set_native_breakpoint, Emulator}, GuestAddr, GuestPhysAddr, QemuExecutor, QemuHooks, QemuInstrumentationFilter, Regs
|
||||||
};
|
};
|
||||||
use rand::{SeedableRng, StdRng, Rng};
|
use rand::{SeedableRng, StdRng, Rng};
|
||||||
use crate::{
|
use crate::{
|
||||||
clock::{ClockTimeFeedback, IcHist, QemuClockIncreaseFeedback, QemuClockObserver, FUZZ_START_TIMESTAMP}, mutational::{InterruptShiftStage, MINIMUM_INTER_ARRIVAL_TIME, input_bytes_to_interrupt_times}, qemustate::QemuStateRestoreHelper, systemstate::{self, feedbacks::{DumpSystraceFeedback, NovelSystemStateFeedback}, stg::{GraphMaximizerCorpusScheduler}, helpers::{QemuSystemStateHelper, ISR_SYMBOLS}, observers::QemuSystemStateObserver, schedulers::{GenerationScheduler, LongestTraceScheduler}, stg::{stg_map_mut_slice, STGEdge, STGNode, StgFeedback, MAX_STG_NUM}}, worst::{AlwaysTrueFeedback, ExecTimeIncFeedback, TimeMaximizerCorpusScheduler, TimeStateMaximizerCorpusScheduler}
|
clock::{ClockTimeFeedback, IcHist, QemuClockIncreaseFeedback, QemuClockObserver, FUZZ_START_TIMESTAMP}, mutational::{InterruptShiftStage, MINIMUM_INTER_ARRIVAL_TIME, input_bytes_to_interrupt_times}, qemustate::QemuStateRestoreHelper, systemstate::{self, feedbacks::{DumpSystraceFeedback, NovelSystemStateFeedback}, stg::{GraphMaximizerCorpusScheduler}, helpers::{QemuSystemStateHelper, ISR_SYMBOLS}, observers::QemuSystemStateObserver, schedulers::{GenerationScheduler, LongestTraceScheduler}, stg::{stg_map_mut_slice, STGEdge, STGNode, StgFeedback, MAX_STG_NUM}}, worst::{AlwaysTrueFeedback, ExecTimeIncFeedback, TimeMaximizerCorpusScheduler, TimeStateMaximizerCorpusScheduler}
|
||||||
};
|
};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@ -24,6 +24,7 @@ use petgraph::graph::EdgeIndex;
|
|||||||
use petgraph::graph::NodeIndex;
|
use petgraph::graph::NodeIndex;
|
||||||
use petgraph::prelude::DiGraph;
|
use petgraph::prelude::DiGraph;
|
||||||
use crate::systemstate::stg::STGFeedbackState;
|
use crate::systemstate::stg::STGFeedbackState;
|
||||||
|
use crate::clock::QEMU_ICOUNT_SHIFT;
|
||||||
|
|
||||||
// Constants ================================================================================
|
// Constants ================================================================================
|
||||||
|
|
||||||
@ -32,41 +33,41 @@ pub static mut RNG_SEED: u64 = 1;
|
|||||||
pub static mut LIMIT : u32 = u32::MAX;
|
pub static mut LIMIT : u32 = u32::MAX;
|
||||||
pub const FIRST_INT : u32 = 500000;
|
pub const FIRST_INT : u32 = 500000;
|
||||||
|
|
||||||
pub const MAX_NUM_INTERRUPT: usize = 32;
|
pub const MAX_NUM_INTERRUPT: usize = 128;
|
||||||
pub const DO_NUM_INTERRUPT: usize = 32;
|
pub const DO_NUM_INTERRUPT: usize = 128;
|
||||||
pub static mut MAX_INPUT_SIZE: usize = 32;
|
pub static mut MAX_INPUT_SIZE: usize = 32;
|
||||||
/// Read ELF program headers to resolve physical load addresses.
|
/// Read ELF program headers to resolve physical load addresses.
|
||||||
fn virt2phys(vaddr: GuestPhysAddr, tab: &EasyElf) -> GuestPhysAddr {
|
fn virt2phys(vaddr: GuestPhysAddr, tab: &EasyElf) -> GuestPhysAddr {
|
||||||
let ret;
|
let ret;
|
||||||
for i in &tab.goblin().program_headers {
|
for i in &tab.goblin().program_headers {
|
||||||
if i.vm_range().contains(&vaddr.try_into().unwrap()) {
|
if i.vm_range().contains(&vaddr.try_into().unwrap()) {
|
||||||
ret = vaddr - TryInto::<GuestPhysAddr>::try_into(i.p_vaddr).unwrap()
|
ret = vaddr - TryInto::<GuestPhysAddr>::try_into(i.p_vaddr).unwrap()
|
||||||
+ TryInto::<GuestPhysAddr>::try_into(i.p_paddr).unwrap();
|
+ TryInto::<GuestPhysAddr>::try_into(i.p_paddr).unwrap();
|
||||||
return ret - (ret % 2);
|
return ret - (ret % 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vaddr;
|
return vaddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_symbol(elf : &EasyElf, symbol : &str, do_translation : bool) -> GuestAddr {
|
pub fn load_symbol(elf : &EasyElf, symbol : &str, do_translation : bool) -> GuestAddr {
|
||||||
try_load_symbol(elf, symbol, do_translation).expect(&format!("Symbol {} not found", symbol))
|
try_load_symbol(elf, symbol, do_translation).expect(&format!("Symbol {} not found", symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_load_symbol(elf : &EasyElf, symbol : &str, do_translation : bool) -> Option<GuestAddr> {
|
pub fn try_load_symbol(elf : &EasyElf, symbol : &str, do_translation : bool) -> Option<GuestAddr> {
|
||||||
let ret = elf
|
let ret = elf
|
||||||
.resolve_symbol(symbol, 0);
|
.resolve_symbol(symbol, 0);
|
||||||
if do_translation {
|
if do_translation {
|
||||||
Option::map_or(ret, None, |x| Some(virt2phys(x as GuestPhysAddr,&elf) as GuestAddr))
|
Option::map_or(ret, None, |x| Some(virt2phys(x as GuestPhysAddr,&elf) as GuestAddr))
|
||||||
} else {ret}
|
} else {ret}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_function_range(elf: &EasyElf, symbol: &str) -> Option<std::ops::Range<GuestAddr>> {
|
pub fn get_function_range(elf: &EasyElf, symbol: &str) -> Option<std::ops::Range<GuestAddr>> {
|
||||||
let gob = elf.goblin();
|
let gob = elf.goblin();
|
||||||
|
|
||||||
let mut funcs : Vec<_> = gob.syms.iter().filter(|x| x.is_function()).collect();
|
let mut funcs : Vec<_> = gob.syms.iter().filter(|x| x.is_function()).collect();
|
||||||
funcs.sort_unstable_by(|x,y| x.st_value.cmp(&y.st_value));
|
funcs.sort_unstable_by(|x,y| x.st_value.cmp(&y.st_value));
|
||||||
|
|
||||||
for sym in &gob.syms {
|
for sym in &gob.syms {
|
||||||
if let Some(sym_name) = gob.strtab.get_at(sym.st_name) {
|
if let Some(sym_name) = gob.strtab.get_at(sym.st_name) {
|
||||||
if sym_name == symbol {
|
if sym_name == symbol {
|
||||||
if sym.st_value == 0 {
|
if sym.st_value == 0 {
|
||||||
@ -87,19 +88,19 @@ pub fn get_function_range(elf: &EasyElf, symbol: &str) -> Option<std::ops::Range
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_fn_symbol_ranges(elf: &EasyElf, api_range: std::ops::Range<GuestAddr>) -> HashMap<String,std::ops::Range<GuestAddr>> {
|
pub fn get_all_fn_symbol_ranges(elf: &EasyElf, api_range: std::ops::Range<GuestAddr>) -> HashMap<String,std::ops::Range<GuestAddr>> {
|
||||||
let mut api_addreses : HashMap<String,std::ops::Range<GuestAddr>> = HashMap::new();
|
let mut api_addreses : HashMap<String,std::ops::Range<GuestAddr>> = HashMap::new();
|
||||||
|
|
||||||
let gob = elf.goblin();
|
let gob = elf.goblin();
|
||||||
|
|
||||||
let mut funcs : Vec<_> = gob.syms.iter().filter(|x| x.is_function() && api_range.contains(&x.st_value.try_into().unwrap())).collect();
|
let mut funcs : Vec<_> = gob.syms.iter().filter(|x| x.is_function() && api_range.contains(&x.st_value.try_into().unwrap())).collect();
|
||||||
funcs.sort_unstable_by(|x,y| x.st_value.cmp(&y.st_value));
|
funcs.sort_unstable_by(|x,y| x.st_value.cmp(&y.st_value));
|
||||||
|
|
||||||
for sym in &funcs {
|
for sym in &funcs {
|
||||||
let sym_name = gob.strtab.get_at(sym.st_name);
|
let sym_name = gob.strtab.get_at(sym.st_name);
|
||||||
if let Some(sym_name) = sym_name {
|
if let Some(sym_name) = sym_name {
|
||||||
// if ISR_SYMBOLS.contains(&sym_name) {continue;}; // skip select symbols, which correspond to ISR-safe system calls
|
// if ISR_SYMBOLS.contains(&sym_name) {continue;}; // skip select symbols, which correspond to ISR-safe system calls
|
||||||
@ -107,17 +108,17 @@ pub fn get_all_fn_symbol_ranges(elf: &EasyElf, api_range: std::ops::Range<GuestA
|
|||||||
api_addreses.insert(sym_name.to_string(), r);
|
api_addreses.insert(sym_name.to_string(), r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i in api_addreses.iter() {
|
for i in api_addreses.iter() {
|
||||||
println!("{} {:#x}..{:#x}", i.0, i.1.start, i.1.end);
|
println!("{} {:#x}..{:#x}", i.0, i.1.start, i.1.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
return api_addreses;
|
return api_addreses;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
static mut libafl_interrupt_offsets : [u32; 32];
|
static mut libafl_interrupt_offsets : [u32; MAX_NUM_INTERRUPT];
|
||||||
static mut libafl_num_interrupts : usize;
|
static mut libafl_num_interrupts : usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Argument parsing ================================================================================
|
// Argument parsing ================================================================================
|
||||||
@ -125,47 +126,47 @@ extern "C" {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Kernel Image
|
/// Kernel Image
|
||||||
#[arg(short, long, value_name = "FILE")]
|
#[arg(short, long, value_name = "FILE")]
|
||||||
kernel: PathBuf,
|
kernel: PathBuf,
|
||||||
|
|
||||||
/// Sets a custom config file
|
/// Sets a custom config file
|
||||||
#[arg(short, long, value_name = "FILE")]
|
#[arg(short, long, value_name = "FILE")]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
|
||||||
/// Sets the prefix of dumed files
|
/// Sets the prefix of dumed files
|
||||||
#[arg(short='n', long, value_name = "FILENAME")]
|
#[arg(short='n', long, value_name = "FILENAME")]
|
||||||
dump_name: Option<PathBuf>,
|
dump_name: Option<PathBuf>,
|
||||||
|
|
||||||
/// do time dumps
|
/// do time dumps
|
||||||
#[arg(short='t', long)]
|
#[arg(short='t', long)]
|
||||||
dump_times: bool,
|
dump_times: bool,
|
||||||
|
|
||||||
/// do worst-case dumps
|
/// do worst-case dumps
|
||||||
#[arg(short='a', long)]
|
#[arg(short='a', long)]
|
||||||
dump_cases: bool,
|
dump_cases: bool,
|
||||||
|
|
||||||
/// do trace dumps (if supported)
|
/// do trace dumps (if supported)
|
||||||
#[arg(short='r', long)]
|
#[arg(short='r', long)]
|
||||||
dump_traces: bool,
|
dump_traces: bool,
|
||||||
|
|
||||||
/// do graph dumps (if supported)
|
/// do graph dumps (if supported)
|
||||||
#[arg(short='g', long)]
|
#[arg(short='g', long)]
|
||||||
dump_graph: bool,
|
dump_graph: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
#[derive(Subcommand,Clone)]
|
#[derive(Subcommand,Clone)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// run a single input
|
/// run a single input
|
||||||
Showmap {
|
Showmap {
|
||||||
/// take this input
|
/// take this input
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
input: PathBuf,
|
input: PathBuf,
|
||||||
},
|
},
|
||||||
/// start fuzzing campaign
|
/// start fuzzing campaign
|
||||||
Fuzz {
|
Fuzz {
|
||||||
/// disable heuristic
|
/// disable heuristic
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
random: bool,
|
random: bool,
|
||||||
@ -175,12 +176,12 @@ enum Commands {
|
|||||||
/// runtime in seconds
|
/// runtime in seconds
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
time: Option<u64>,
|
time: Option<u64>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a state, cli and a suffix, writes out the current worst case
|
/// Takes a state, cli and a suffix, writes out the current worst case
|
||||||
macro_rules! do_dump_case {
|
macro_rules! do_dump_case {
|
||||||
( $s:expr,$cli:expr, $c:expr) => {
|
( $s:expr,$cli:expr, $c:expr) => {
|
||||||
if ($cli.dump_cases) {
|
if ($cli.dump_cases) {
|
||||||
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"case"} else {$c});
|
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"case"} else {$c});
|
||||||
println!("Dumping worst case to {:?}", &dump_path);
|
println!("Dumping worst case to {:?}", &dump_path);
|
||||||
@ -198,12 +199,12 @@ macro_rules! do_dump_case {
|
|||||||
fs::write(dump_path,wi).expect("Failed to write worst corpus element");
|
fs::write(dump_path,wi).expect("Failed to write worst corpus element");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a state, cli and a suffix, appends icount history
|
/// Takes a state, cli and a suffix, appends icount history
|
||||||
macro_rules! do_dump_times {
|
macro_rules! do_dump_times {
|
||||||
($state:expr, $cli:expr, $c:expr) => {
|
($state:expr, $cli:expr, $c:expr) => {
|
||||||
if $cli.dump_times {
|
if $cli.dump_times {
|
||||||
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"time"} else {$c});
|
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"time"} else {$c});
|
||||||
let mut file = std::fs::OpenOptions::new()
|
let mut file = std::fs::OpenOptions::new()
|
||||||
@ -218,12 +219,12 @@ macro_rules! do_dump_times {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a state and a bool, writes out the current graph
|
/// Takes a state and a bool, writes out the current graph
|
||||||
macro_rules! do_dump_stg {
|
macro_rules! do_dump_stg {
|
||||||
($state:expr, $cli:expr, $c:expr) => {
|
($state:expr, $cli:expr, $c:expr) => {
|
||||||
#[cfg(feature = "trace_stg")]
|
#[cfg(feature = "trace_stg")]
|
||||||
if $cli.dump_graph {
|
if $cli.dump_graph {
|
||||||
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"stg"} else {$c});
|
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"stg"} else {$c});
|
||||||
@ -236,12 +237,12 @@ macro_rules! do_dump_stg {
|
|||||||
fs::write(dump_path,outs).expect("Failed to write graph");
|
fs::write(dump_path,outs).expect("Failed to write graph");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a state and a bool, writes out top rated inputs
|
/// Takes a state and a bool, writes out top rated inputs
|
||||||
macro_rules! do_dump_toprated {
|
macro_rules! do_dump_toprated {
|
||||||
($state:expr, $cli:expr, $c:expr) => {
|
($state:expr, $cli:expr, $c:expr) => {
|
||||||
if $cli.dump_cases {
|
if $cli.dump_cases {
|
||||||
{
|
{
|
||||||
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"toprated"} else {$c});
|
let dump_path = $cli.dump_name.clone().unwrap().with_extension(if $c=="" {"toprated"} else {$c});
|
||||||
@ -254,12 +255,12 @@ macro_rules! do_dump_toprated {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_from_config(kernel : &PathBuf, path : &PathBuf) {
|
fn env_from_config(kernel : &PathBuf, path : &PathBuf) {
|
||||||
let is_csv = path.as_path().extension().map_or(false, |x| x=="csv");
|
let is_csv = path.as_path().extension().map_or(false, |x| x=="csv");
|
||||||
if !is_csv {
|
if !is_csv {
|
||||||
let lines = std::fs::read_to_string(path).expect("Config file not found");
|
let lines = std::fs::read_to_string(path).expect("Config file not found");
|
||||||
let lines = lines.lines().filter(
|
let lines = lines.lines().filter(
|
||||||
|x| x.len()>0
|
|x| x.len()>0
|
||||||
@ -268,7 +269,7 @@ fn env_from_config(kernel : &PathBuf, path : &PathBuf) {
|
|||||||
let pair = l.split_once('=').expect("Non VAR=VAL line in config");
|
let pair = l.split_once('=').expect("Non VAR=VAL line in config");
|
||||||
std::env::set_var(pair.0, pair.1);
|
std::env::set_var(pair.0, pair.1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut reader = csv::Reader::from_path(path).expect("CSV read from config failed");
|
let mut reader = csv::Reader::from_path(path).expect("CSV read from config failed");
|
||||||
let p = kernel.as_path();
|
let p = kernel.as_path();
|
||||||
let stem = p.file_stem().expect("Kernel filename error").to_str().unwrap();
|
let stem = p.file_stem().expect("Kernel filename error").to_str().unwrap();
|
||||||
@ -282,127 +283,132 @@ fn env_from_config(kernel : &PathBuf, path : &PathBuf) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzzer setup ================================================================================
|
// Fuzzer setup ================================================================================
|
||||||
|
|
||||||
pub fn fuzz() {
|
pub fn fuzz() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
env_from_config(&cli.kernel, &cli.config);
|
env_from_config(&cli.kernel, &cli.config);
|
||||||
unsafe {FUZZ_START_TIMESTAMP = SystemTime::now();}
|
unsafe {FUZZ_START_TIMESTAMP = SystemTime::now();}
|
||||||
if cli.dump_name.is_none() && (cli.dump_times || cli.dump_cases || cli.dump_traces || cli.dump_graph) {
|
if cli.dump_name.is_none() && (cli.dump_times || cli.dump_cases || cli.dump_traces || cli.dump_graph) {
|
||||||
panic!("Dump name not give but dump is requested");
|
panic!("Dump name not give but dump is requested");
|
||||||
}
|
}
|
||||||
let mut starttime = std::time::Instant::now();
|
let mut starttime = std::time::Instant::now();
|
||||||
// Hardcoded parameters
|
// Hardcoded parameters
|
||||||
let timeout = Duration::from_secs(10);
|
let timeout = Duration::from_secs(10);
|
||||||
let broker_port = 1337;
|
let broker_port = 1337;
|
||||||
let cores = Cores::from_cmdline("1").unwrap();
|
let cores = Cores::from_cmdline("1").unwrap();
|
||||||
let corpus_dirs = [PathBuf::from("./corpus")];
|
let corpus_dirs = [PathBuf::from("./corpus")];
|
||||||
let objective_dir = PathBuf::from("./crashes");
|
let objective_dir = PathBuf::from("./crashes");
|
||||||
|
|
||||||
let mut elf_buffer = Vec::new();
|
let mut elf_buffer = Vec::new();
|
||||||
let elf = EasyElf::from_file(
|
let elf = EasyElf::from_file(
|
||||||
&cli.kernel,
|
&cli.kernel,
|
||||||
&mut elf_buffer,
|
&mut elf_buffer,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// the main address where the fuzzer starts
|
// the main address where the fuzzer starts
|
||||||
// if this is set for freeRTOS it has an influence on where the data will have to be written,
|
// if this is set for freeRTOS it has an influence on where the data will have to be written,
|
||||||
// since the startup routine copies the data segemnt to it's virtual address
|
// since the startup routine copies the data segemnt to it's virtual address
|
||||||
let main_addr = elf
|
let main_addr = elf
|
||||||
.resolve_symbol(&env::var("FUZZ_MAIN").unwrap_or_else(|_| "FUZZ_MAIN".to_owned()), 0);
|
.resolve_symbol(&env::var("FUZZ_MAIN").unwrap_or_else(|_| "FUZZ_MAIN".to_owned()), 0);
|
||||||
if let Some(main_addr) = main_addr {
|
if let Some(main_addr) = main_addr {
|
||||||
println!("main address = {:#x}", main_addr);
|
println!("main address = {:#x}", main_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_addr = load_symbol(&elf, &env::var("FUZZ_INPUT").unwrap_or_else(|_| "FUZZ_INPUT".to_owned()), true);
|
let input_addr = load_symbol(&elf, &env::var("FUZZ_INPUT").unwrap_or_else(|_| "FUZZ_INPUT".to_owned()), true);
|
||||||
println!("FUZZ_INPUT @ {:#x}", input_addr);
|
println!("FUZZ_INPUT @ {:#x}", input_addr);
|
||||||
|
|
||||||
let input_length_ptr = try_load_symbol(&elf, &env::var("FUZZ_LENGTH").unwrap_or_else(|_| "FUZZ_LENGTH".to_owned()), true);
|
let input_length_ptr = try_load_symbol(&elf, &env::var("FUZZ_LENGTH").unwrap_or_else(|_| "FUZZ_LENGTH".to_owned()), true);
|
||||||
let input_counter_ptr = try_load_symbol(&elf, &env::var("FUZZ_POINTER").unwrap_or_else(|_| "FUZZ_POINTER".to_owned()), true);
|
let input_counter_ptr = try_load_symbol(&elf, &env::var("FUZZ_POINTER").unwrap_or_else(|_| "FUZZ_POINTER".to_owned()), true);
|
||||||
|
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let curr_tcb_pointer = load_symbol(&elf, "pxCurrentTCB", false); // loads to the address specified in elf, without respecting program headers
|
let curr_tcb_pointer = load_symbol(&elf, "pxCurrentTCB", false); // loads to the address specified in elf, without respecting program headers
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
println!("TCB pointer at {:#x}", curr_tcb_pointer);
|
println!("TCB pointer at {:#x}", curr_tcb_pointer);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let task_queue_addr = load_symbol(&elf, "pxReadyTasksLists", false);
|
let task_queue_addr = load_symbol(&elf, "pxReadyTasksLists", false);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let task_delay_addr = load_symbol(&elf, "pxDelayedTaskList", false);
|
let task_delay_addr = load_symbol(&elf, "pxDelayedTaskList", false);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let task_delay_overflow_addr = load_symbol(&elf, "pxOverflowDelayedTaskList", false);
|
let task_delay_overflow_addr = load_symbol(&elf, "pxOverflowDelayedTaskList", false);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let scheduler_lock = load_symbol(&elf, "uxSchedulerSuspended", false);
|
let scheduler_lock = load_symbol(&elf, "uxSchedulerSuspended", false);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let scheduler_running = load_symbol(&elf, "xSchedulerRunning", false);
|
let scheduler_running = load_symbol(&elf, "xSchedulerRunning", false);
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let critical_section = load_symbol(&elf, "uxCriticalNesting", false);
|
let critical_section = load_symbol(&elf, "uxCriticalNesting", false);
|
||||||
let app_start = load_symbol(&elf, "__APP_CODE_START__", false);
|
let app_start = load_symbol(&elf, "__APP_CODE_START__", false);
|
||||||
let app_end = load_symbol(&elf, "__APP_CODE_END__", false);
|
let app_end = load_symbol(&elf, "__APP_CODE_END__", false);
|
||||||
let app_range = app_start..app_end;
|
let app_range = app_start..app_end;
|
||||||
let api_start = load_symbol(&elf, "__API_CODE_START__", false);
|
let api_start = load_symbol(&elf, "__API_CODE_START__", false);
|
||||||
let api_end = load_symbol(&elf, "__API_CODE_END__", false);
|
let api_end = load_symbol(&elf, "__API_CODE_END__", false);
|
||||||
let api_range = api_start..api_end;
|
let api_range = api_start..api_end;
|
||||||
|
|
||||||
let breakpoint = elf
|
let breakpoint = elf
|
||||||
.resolve_symbol(
|
.resolve_symbol(
|
||||||
&env::var("BREAKPOINT").unwrap_or_else(|_| "BREAKPOINT".to_owned()),
|
&env::var("BREAKPOINT").unwrap_or_else(|_| "BREAKPOINT".to_owned()),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
.expect("Symbol or env BREAKPOINT not found");
|
.expect("Symbol or env BREAKPOINT not found");
|
||||||
println!("Breakpoint address = {:#x}", breakpoint);
|
println!("Breakpoint address = {:#x}", breakpoint);
|
||||||
unsafe {
|
unsafe {
|
||||||
libafl_num_interrupts = 0;
|
libafl_num_interrupts = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(input_len) = env::var("FUZZ_INPUT_LEN") {
|
if let Ok(input_len) = env::var("FUZZ_INPUT_LEN") {
|
||||||
unsafe {MAX_INPUT_SIZE = str::parse::<usize>(&input_len).expect("FUZZ_INPUT_LEN was not a number");}
|
unsafe {MAX_INPUT_SIZE = str::parse::<usize>(&input_len).expect("FUZZ_INPUT_LEN was not a number");}
|
||||||
}
|
}
|
||||||
unsafe {dbg!(MAX_INPUT_SIZE);}
|
unsafe {dbg!(MAX_INPUT_SIZE);}
|
||||||
|
|
||||||
if let Ok(seed) = env::var("SEED_RANDOM") {
|
if let Ok(seed) = env::var("SEED_RANDOM") {
|
||||||
unsafe {RNG_SEED = str::parse::<u64>(&seed).expect("SEED_RANDOM must be an integer.");}
|
unsafe {RNG_SEED = str::parse::<u64>(&seed).expect("SEED_RANDOM must be an integer.");}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut api_ranges = get_all_fn_symbol_ranges(&elf, api_range);
|
let mut api_ranges = get_all_fn_symbol_ranges(&elf, api_range);
|
||||||
|
let app_fn_ranges = get_all_fn_symbol_ranges(&elf, app_range.clone());
|
||||||
|
|
||||||
let mut isr_ranges : HashMap<String,std::ops::Range<GuestAddr>> = systemstate::helpers::ISR_SYMBOLS.iter().filter_map(|x| (api_ranges.get(&x.to_string()).map(|y| (x.to_string(),y.clone())))).collect();
|
let mut isr_ranges : HashMap<String,std::ops::Range<GuestAddr>> = systemstate::helpers::ISR_SYMBOLS.iter().filter_map(|x| (api_ranges.get(&x.to_string()).map(|y| (x.to_string(),y.clone())))).collect();
|
||||||
let denylist=isr_ranges.values().map(|x| x.clone()).collect();
|
systemstate::helpers::ISR_SYMBOLS.iter().for_each(|x| {let _ =(app_fn_ranges.get(&x.to_string()).map(|y| (x.to_string(),y.clone()))).map(|y| isr_ranges.insert(y.0,y.1));}); // add used defined isr
|
||||||
let denylist = QemuInstrumentationFilter::DenyList(denylist); // do not count isr jumps, which are useless
|
let denylist=isr_ranges.values().map(|x| x.clone()).collect();
|
||||||
#[cfg(feature = "observe_systemstate")]
|
let denylist = QemuInstrumentationFilter::DenyList(denylist); // do not count isr jumps, which are useless
|
||||||
let mut isr_addreses : HashMap<GuestAddr, String> = systemstate::helpers::ISR_SYMBOLS.iter().filter_map(|x| (api_ranges.remove(&x.to_string()).map(|y| (y.start,x.to_string())))).collect();
|
#[cfg(feature = "observe_systemstate")]
|
||||||
|
let mut isr_addreses : HashMap<GuestAddr, String> = systemstate::helpers::ISR_SYMBOLS.iter().filter_map(|x| (api_ranges.remove(&x.to_string()).map(|y| (y.start,x.to_string())))).collect();
|
||||||
|
systemstate::helpers::ISR_SYMBOLS.iter().for_each(|x| {let _ =(app_fn_ranges.get(&x.to_string()).map(|y| (x.to_string(),y.clone()))).map(|y| isr_addreses.insert(y.1.start, y.0));}); // add used defined isr
|
||||||
|
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
for i in systemstate::helpers::ISR_SYMBOLS {
|
for i in systemstate::helpers::ISR_SYMBOLS {
|
||||||
if isr_ranges.get(&i.to_string()).is_none() {
|
if isr_ranges.get(&i.to_string()).is_none() {
|
||||||
if let Some(fr) = get_function_range(&elf, i) {
|
if let Some(fr) = get_function_range(&elf, i) {
|
||||||
isr_addreses.insert(fr.start, i.to_string());
|
isr_addreses.insert(fr.start, i.to_string());
|
||||||
isr_ranges.insert(i.to_string(), fr);
|
isr_ranges.insert(i.to_string(), fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let api_addreses : HashMap<GuestAddr, String> = api_ranges.iter().map(|(k,v)| (v.start,k.clone())).collect();
|
let api_addreses : HashMap<GuestAddr, String> = api_ranges.iter().map(|(k,v)| (v.start,k.clone())).collect();
|
||||||
|
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let api_ranges : Vec<_> = api_ranges.into_iter().collect();
|
let api_ranges : Vec<_> = api_ranges.into_iter().collect();
|
||||||
#[cfg(feature = "observe_systemstate")]
|
#[cfg(feature = "observe_systemstate")]
|
||||||
let isr_ranges : Vec<_> = isr_ranges.into_iter().collect();
|
let isr_ranges : Vec<_> = isr_ranges.into_iter().collect();
|
||||||
|
|
||||||
// Client setup ================================================================================
|
// Client setup ================================================================================
|
||||||
|
|
||||||
let mut run_client = |state: Option<_>, mut mgr, _core_id| {
|
let mut run_client = |state: Option<_>, mut mgr, _core_id| {
|
||||||
// Initialize QEMU
|
// Initialize QEMU
|
||||||
let args: Vec<String> = vec![
|
let args: Vec<String> = vec![
|
||||||
"target/debug/fret",
|
"target/debug/fret",
|
||||||
"-icount",
|
"-icount",
|
||||||
"shift=4,align=off,sleep=off",
|
&format!("shift={},align=off,sleep=off", QEMU_ICOUNT_SHIFT),
|
||||||
"-machine",
|
"-machine",
|
||||||
"mps2-an385",
|
"mps2-an385",
|
||||||
|
"-cpu",
|
||||||
|
"cortex-m3",
|
||||||
"-monitor",
|
"-monitor",
|
||||||
"null",
|
"null",
|
||||||
"-kernel",
|
"-kernel",
|
||||||
@ -411,9 +417,9 @@ pub fn fuzz() {
|
|||||||
"null",
|
"null",
|
||||||
"-nographic",
|
"-nographic",
|
||||||
"-S",
|
"-S",
|
||||||
"-semihosting",
|
// "-semihosting",
|
||||||
"--semihosting-config",
|
// "--semihosting-config",
|
||||||
"enable=on,target=native",
|
// "enable=on,target=native",
|
||||||
"-snapshot",
|
"-snapshot",
|
||||||
"-drive",
|
"-drive",
|
||||||
"if=none,format=qcow2,file=dummy.qcow2",
|
"if=none,format=qcow2,file=dummy.qcow2",
|
||||||
@ -447,6 +453,9 @@ pub fn fuzz() {
|
|||||||
buf = &buf[libafl_num_interrupts*4..];
|
buf = &buf[libafl_num_interrupts*4..];
|
||||||
len = buf.len();
|
len = buf.len();
|
||||||
}
|
}
|
||||||
|
// for i in 0 .. libafl_num_interrupts {
|
||||||
|
// libafl_interrupt_offsets[i] = FIRST_INT+TryInto::<u32>::try_into(i).unwrap()*MINIMUM_INTER_ARRIVAL_TIME;
|
||||||
|
// }
|
||||||
// println!("Load: {:?}", libafl_interrupt_offsets[0..libafl_num_interrupts].to_vec());
|
// println!("Load: {:?}", libafl_interrupt_offsets[0..libafl_num_interrupts].to_vec());
|
||||||
}
|
}
|
||||||
if len > MAX_INPUT_SIZE {
|
if len > MAX_INPUT_SIZE {
|
||||||
|
@ -13,9 +13,9 @@ use libafl::{
|
|||||||
corpus::{self, Corpus}, fuzzer::Evaluator, mark_feature_time, prelude::{new_hash_feedback, CorpusId, HasBytesVec, MutationResult, Mutator, UsesInput}, stages::Stage, start_timer, state::{HasCorpus, HasMetadata, HasNamedMetadata, HasRand, MaybeHasClientPerfMonitor, UsesState}, Error
|
corpus::{self, Corpus}, fuzzer::Evaluator, mark_feature_time, prelude::{new_hash_feedback, CorpusId, HasBytesVec, MutationResult, Mutator, UsesInput}, stages::Stage, start_timer, state::{HasCorpus, HasMetadata, HasNamedMetadata, HasRand, MaybeHasClientPerfMonitor, UsesState}, Error
|
||||||
};
|
};
|
||||||
use libafl::prelude::State;
|
use libafl::prelude::State;
|
||||||
use crate::{clock::IcHist, fuzzer::{DO_NUM_INTERRUPT, FIRST_INT}, systemstate::{stg::{STGFeedbackState, STGNodeMetadata}, CaptureEvent, ExecInterval, FreeRTOSSystemStateMetadata, ReducedFreeRTOSSystemState}};
|
use crate::{clock::{IcHist, QEMU_ISNS_PER_USEC}, fuzzer::{DO_NUM_INTERRUPT, FIRST_INT}, systemstate::{stg::{STGFeedbackState, STGNodeMetadata}, CaptureEvent, ExecInterval, FreeRTOSSystemStateMetadata, ReducedFreeRTOSSystemState}};
|
||||||
|
|
||||||
pub static mut MINIMUM_INTER_ARRIVAL_TIME : u32 = 1000 /*ms*/ * 62500;
|
pub static mut MINIMUM_INTER_ARRIVAL_TIME : u32 = 1000 /*us*/ * QEMU_ISNS_PER_USEC;
|
||||||
// one isn per 2**4 ns
|
// one isn per 2**4 ns
|
||||||
// virtual insn/sec 62500000 = 1/16 GHz
|
// virtual insn/sec 62500000 = 1/16 GHz
|
||||||
// 1ms = 62500 insn
|
// 1ms = 62500 insn
|
||||||
|
@ -109,9 +109,9 @@ where
|
|||||||
where
|
where
|
||||||
QT: QemuHelperTuple<S>,
|
QT: QemuHelperTuple<S>,
|
||||||
{
|
{
|
||||||
for wp in self.api_fn_addrs.keys() {
|
// for wp in self.api_fn_addrs.keys() {
|
||||||
_hooks.instruction(*wp, Hook::Function(exec_syscall_hook::<QT, S>), false);
|
// _hooks.instruction(*wp, Hook::Function(exec_syscall_hook::<QT, S>), false);
|
||||||
}
|
// }
|
||||||
for wp in self.isr_addrs.keys() {
|
for wp in self.isr_addrs.keys() {
|
||||||
_hooks.instruction(*wp, Hook::Function(exec_isr_hook::<QT, S>), false);
|
_hooks.instruction(*wp, Hook::Function(exec_isr_hook::<QT, S>), false);
|
||||||
}
|
}
|
||||||
@ -213,7 +213,11 @@ fn trigger_collection(emulator: &Emulator, edge: (Option<GuestAddr>,Option<Guest
|
|||||||
println!("API Not found: {:#x} {:#x}", src, dest);
|
println!("API Not found: {:#x} {:#x}", src, dest);
|
||||||
}
|
}
|
||||||
} else if let Some(s) = h.api_fn_addrs.get(&dest) { // API Call
|
} else if let Some(s) = h.api_fn_addrs.get(&dest) { // API Call
|
||||||
|
// if let None = in_any_range(&h.isr_ranges, src) {
|
||||||
systemstate.capture_point=(CaptureEvent::APIStart, s.to_string());
|
systemstate.capture_point=(CaptureEvent::APIStart, s.to_string());
|
||||||
|
// } else {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
} else {
|
} else {
|
||||||
println!("API Not found: {:#x}", src);
|
println!("API Not found: {:#x}", src);
|
||||||
}
|
}
|
||||||
@ -315,14 +319,13 @@ where
|
|||||||
let mut edge = (None, Some(pc));
|
let mut edge = (None, Some(pc));
|
||||||
unsafe {
|
unsafe {
|
||||||
LAST_API_CALL.with(|x| {
|
LAST_API_CALL.with(|x| {
|
||||||
match *x.get() {
|
match (*x.get()).take() {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
edge.0=Some(s.0);
|
edge.0=Some(s.0);
|
||||||
trigger_collection(emulator, edge, h);
|
trigger_collection(emulator, edge, h);
|
||||||
},
|
},
|
||||||
None => (),
|
None => (),
|
||||||
}
|
}
|
||||||
*x.get()=None;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,11 +346,13 @@ where
|
|||||||
QT: QemuHelperTuple<S>,
|
QT: QemuHelperTuple<S>,
|
||||||
{
|
{
|
||||||
if let Some(h) = hooks.helpers().match_first_type::<QemuSystemStateHelper>() {
|
if let Some(h) = hooks.helpers().match_first_type::<QemuSystemStateHelper>() {
|
||||||
if h.app_range.contains(&src) && !h.app_range.contains(&dest) {
|
if h.app_range.contains(&src) && !h.app_range.contains(&dest) && in_any_range(&h.isr_ranges,src).is_none() {
|
||||||
if let Some(_) = in_any_range(&h.api_fn_ranges,dest) {
|
if let Some(_) = in_any_range(&h.api_fn_ranges,dest) {
|
||||||
// println!("New jmp {:x} {:x}", src, dest);
|
// println!("New jmp {:x} {:x}", src, dest);
|
||||||
// println!("API Call Edge {:x} {:x}", src, dest);
|
// println!("API Call Edge {:x} {:x}", src, dest);
|
||||||
return Some(1);
|
return Some(1);
|
||||||
|
// TODO: trigger collection right here
|
||||||
|
// otherwise there can be a race-condition, where LAST_API_CALL is set before the api starts, if the interrupt handler calls an api function, it will misidentify the callsite of that api call
|
||||||
}
|
}
|
||||||
} else if dest == 0 { // !h.app_range.contains(&src) &&
|
} else if dest == 0 { // !h.app_range.contains(&src) &&
|
||||||
if let Some(_) = in_any_range(&h.api_fn_ranges, src) {
|
if let Some(_) = in_any_range(&h.api_fn_ranges, src) {
|
||||||
@ -363,6 +368,18 @@ where
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_icount(emulator : &Emulator) -> u64 {
|
||||||
|
unsafe {
|
||||||
|
// TODO: investigate why can_do_io is not set sometimes, as this is just a workaround
|
||||||
|
let c = emulator.cpu_from_index(0);
|
||||||
|
let can_do_io = (*c.raw_ptr()).neg.can_do_io;
|
||||||
|
(*c.raw_ptr()).neg.can_do_io = true;
|
||||||
|
let r = emu::icount_get_raw();
|
||||||
|
(*c.raw_ptr()).neg.can_do_io = can_do_io;
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trace_api_call<QT, S>(
|
pub fn trace_api_call<QT, S>(
|
||||||
hooks: &mut QemuHooks<QT, S>,
|
hooks: &mut QemuHooks<QT, S>,
|
||||||
_state: Option<&mut S>,
|
_state: Option<&mut S>,
|
||||||
@ -373,11 +390,10 @@ where
|
|||||||
QT: QemuHelperTuple<S>,
|
QT: QemuHelperTuple<S>,
|
||||||
{
|
{
|
||||||
if id == 1 { // API call
|
if id == 1 { // API call
|
||||||
unsafe {
|
let h = hooks.helpers().match_first_type::<QemuSystemStateHelper>().expect("QemuSystemHelper not found in helper tupel");
|
||||||
let p = LAST_API_CALL.with(|x| x.get());
|
let emulator = hooks.emulator();
|
||||||
*p = Some((src,dest));
|
trigger_collection(emulator, (Some(src), Some(dest)), h);
|
||||||
// println!("Jump {:#x} {:#x}", src, dest);
|
// println!("Exec API Call {:#x} {:#x} {}", src, dest, get_icount(emulator));
|
||||||
}
|
|
||||||
} else if id == 2 { // API return
|
} else if id == 2 { // API return
|
||||||
let h = hooks.helpers().match_first_type::<QemuSystemStateHelper>().expect("QemuSystemHelper not found in helper tupel");
|
let h = hooks.helpers().match_first_type::<QemuSystemStateHelper>().expect("QemuSystemHelper not found in helper tupel");
|
||||||
// Ignore returns to other APIs or ISRs. We only account for the first call depth of API calls from user space.
|
// Ignore returns to other APIs or ISRs. We only account for the first call depth of API calls from user space.
|
||||||
@ -389,7 +405,7 @@ where
|
|||||||
edge.1=Some(dest);
|
edge.1=Some(dest);
|
||||||
|
|
||||||
trigger_collection(emulator, edge, h);
|
trigger_collection(emulator, edge, h);
|
||||||
// println!("Exec API Return Edge {:#x} {:#x}", src, dest);
|
// println!("Exec API Return Edge {:#x} {:#x} {}", src, dest, get_icount(emulator));
|
||||||
}
|
}
|
||||||
} else if id == 3 { // ISR return
|
} else if id == 3 { // ISR return
|
||||||
let h = hooks.helpers().match_first_type::<QemuSystemStateHelper>().expect("QemuSystemHelper not found in helper tupel");
|
let h = hooks.helpers().match_first_type::<QemuSystemStateHelper>().expect("QemuSystemHelper not found in helper tupel");
|
||||||
@ -399,7 +415,7 @@ where
|
|||||||
edge.0=Some(in_any_range(&h.isr_ranges, src).unwrap().start);
|
edge.0=Some(in_any_range(&h.isr_ranges, src).unwrap().start);
|
||||||
|
|
||||||
trigger_collection(emulator, edge, h);
|
trigger_collection(emulator, edge, h);
|
||||||
// println!("Exec ISR Return Edge {:#x} {:#x}", src, dest);
|
// println!("Exec ISR Return Edge {:#x} {:#x} {}", src, dest, get_icount(emulator));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ where
|
|||||||
// unsafe {self.last_run = invalidate_ineffective_isr(refine_system_states(&mut CURRENT_SYSTEMSTATE_VEC));}
|
// unsafe {self.last_run = invalidate_ineffective_isr(refine_system_states(&mut CURRENT_SYSTEMSTATE_VEC));}
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut temp = refine_system_states(&mut CURRENT_SYSTEMSTATE_VEC);
|
let mut temp = refine_system_states(&mut CURRENT_SYSTEMSTATE_VEC);
|
||||||
fix_broken_trace(&mut temp.1);
|
// fix_broken_trace(&mut temp.1);
|
||||||
self.last_run = temp.0.clone();
|
self.last_run = temp.0.clone();
|
||||||
// println!("{:?}",temp);
|
// println!("{:?}",temp);
|
||||||
let mut temp = states2intervals(temp.0, temp.1);
|
let mut temp = states2intervals(temp.0, temp.1);
|
||||||
@ -154,6 +154,7 @@ fn refine_system_states(input: &mut Vec<RawFreeRTOSSystemState>) -> (Vec<Reduced
|
|||||||
delay_list_after: delay_list,
|
delay_list_after: delay_list,
|
||||||
// input_counter: i.input_counter,//+IRQ_INPUT_BYTES_NUMBER,
|
// input_counter: i.input_counter,//+IRQ_INPUT_BYTES_NUMBER,
|
||||||
});
|
});
|
||||||
|
// println!("Refine: {:?} {:?} {:x}-{:x}",i.capture_point.0, i.capture_point.1.to_string(), i.edge.0.unwrap_or(0), i.edge.1.unwrap_or(0));
|
||||||
ret.1.push((i.qemu_tick, i.capture_point.0, i.capture_point.1.to_string(), i.edge));
|
ret.1.push((i.qemu_tick, i.capture_point.0, i.capture_point.1.to_string(), i.edge));
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
@ -161,6 +162,7 @@ fn refine_system_states(input: &mut Vec<RawFreeRTOSSystemState>) -> (Vec<Reduced
|
|||||||
|
|
||||||
/// Transform the states and metadata into a list of ExecIntervals
|
/// Transform the states and metadata into a list of ExecIntervals
|
||||||
fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, CaptureEvent, String, (Option<u32>, Option<u32>))>) -> (Vec<ExecInterval>, HashMap<u64, ReducedFreeRTOSSystemState>) {
|
fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, CaptureEvent, String, (Option<u32>, Option<u32>))>) -> (Vec<ExecInterval>, HashMap<u64, ReducedFreeRTOSSystemState>) {
|
||||||
|
if trace.len() == 0 {return (Vec::new(), HashMap::new());}
|
||||||
let mut isr_stack : VecDeque<u8> = VecDeque::from([]); // 2+ = ISR, 1 = systemcall, 0 = APP. Trace starts with an ISREnd and executes the app
|
let mut isr_stack : VecDeque<u8> = VecDeque::from([]); // 2+ = ISR, 1 = systemcall, 0 = APP. Trace starts with an ISREnd and executes the app
|
||||||
|
|
||||||
|
|
||||||
@ -179,11 +181,11 @@ fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, Capt
|
|||||||
level_of_task.insert(curr_name, 0);
|
level_of_task.insert(curr_name, 0);
|
||||||
}
|
}
|
||||||
*level_of_task.get_mut(curr_name).unwrap()=0;
|
*level_of_task.get_mut(curr_name).unwrap()=0;
|
||||||
&0
|
0
|
||||||
},
|
},
|
||||||
CaptureEvent::APIStart => { // API start can only be called in the app
|
CaptureEvent::APIStart => { // API start can only be called in the app
|
||||||
*level_of_task.get_mut(curr_name).unwrap()=1;
|
*level_of_task.get_mut(curr_name).unwrap()=1;
|
||||||
&1
|
1
|
||||||
},
|
},
|
||||||
CaptureEvent::ISREnd => {
|
CaptureEvent::ISREnd => {
|
||||||
// special case where the next block is an app start
|
// special case where the next block is an app start
|
||||||
@ -192,12 +194,12 @@ fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, Capt
|
|||||||
}
|
}
|
||||||
// nested isr, TODO: Test level > 2
|
// nested isr, TODO: Test level > 2
|
||||||
if isr_stack.len() > 1 {
|
if isr_stack.len() > 1 {
|
||||||
isr_stack.pop_back();
|
isr_stack.pop_back().unwrap();
|
||||||
isr_stack.back().unwrap()
|
*isr_stack.back().unwrap()
|
||||||
} else {
|
} else {
|
||||||
isr_stack.pop_back();
|
isr_stack.pop_back();
|
||||||
// possibly go back to an api call that is still running for this task
|
// possibly go back to an api call that is still running for this task
|
||||||
level_of_task.get(curr_name).unwrap()
|
*level_of_task.get(curr_name).unwrap()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CaptureEvent::ISRStart => {
|
CaptureEvent::ISRStart => {
|
||||||
@ -207,16 +209,16 @@ fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, Capt
|
|||||||
// } else {
|
// } else {
|
||||||
// regular case
|
// regular case
|
||||||
if isr_stack.len() > 0 {
|
if isr_stack.len() > 0 {
|
||||||
let l = isr_stack.back().unwrap();
|
let l = *isr_stack.back().unwrap();
|
||||||
isr_stack.push_back(*l);
|
isr_stack.push_back(l+1);
|
||||||
isr_stack.back().unwrap()
|
l+1
|
||||||
} else {
|
} else {
|
||||||
isr_stack.push_back(2);
|
isr_stack.push_back(2);
|
||||||
&2
|
2
|
||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
_ => &100
|
_ => 100
|
||||||
};
|
};
|
||||||
// if trace[i].2 == CaptureEvent::End {break;}
|
// if trace[i].2 == CaptureEvent::End {break;}
|
||||||
let next_hash=trace[i+1].get_hash();
|
let next_hash=trace[i+1].get_hash();
|
||||||
@ -230,7 +232,7 @@ fn states2intervals(trace: Vec<ReducedFreeRTOSSystemState>, meta: Vec<(u64, Capt
|
|||||||
end_state: next_hash,
|
end_state: next_hash,
|
||||||
start_capture: (meta[i].1, meta[i].2.clone()),
|
start_capture: (meta[i].1, meta[i].2.clone()),
|
||||||
end_capture: (meta[i+1].1, meta[i+1].2.clone()),
|
end_capture: (meta[i+1].1, meta[i+1].2.clone()),
|
||||||
level: *level,
|
level: level,
|
||||||
tick_spend_preempted: 0,
|
tick_spend_preempted: 0,
|
||||||
abb: None
|
abb: None
|
||||||
});
|
});
|
||||||
@ -250,8 +252,9 @@ fn add_abb_info(trace: &mut Vec<ExecInterval>, table: &HashMap<u64, ReducedFreeR
|
|||||||
let curr_name = &table[&trace[i].start_state].current_task.task_name;
|
let curr_name = &table[&trace[i].start_state].current_task.task_name;
|
||||||
// let last : Option<&usize> = last_abb_start_of_task.get(&curr_name);
|
// let last : Option<&usize> = last_abb_start_of_task.get(&curr_name);
|
||||||
|
|
||||||
let open_abb = open_abb_at_this_task_or_level.get(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()})).to_owned(); // apps/apis are differentiated by task name, isrs by nested level
|
let open_abb = open_abb_at_this_task_or_level.get(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""})).to_owned(); // apps/apis are differentiated by task name, isrs by nested level
|
||||||
|
|
||||||
|
// println!("Edge {:x}-{:x}", edges[i].0.unwrap_or(0xffff), edges[i].1.unwrap_or(0xffff));
|
||||||
|
|
||||||
match (&trace[i].start_capture.0, &trace[i].end_capture.0) {
|
match (&trace[i].start_capture.0, &trace[i].end_capture.0) {
|
||||||
// case with abb to block correspondence
|
// case with abb to block correspondence
|
||||||
@ -269,35 +272,38 @@ fn add_abb_info(trace: &mut Vec<ExecInterval>, table: &HashMap<u64, ReducedFreeR
|
|||||||
match trace[i].start_capture.0 {
|
match trace[i].start_capture.0 {
|
||||||
// generic api abb start
|
// generic api abb start
|
||||||
CaptureEvent::APIStart => {
|
CaptureEvent::APIStart => {
|
||||||
// assert_eq!(open_abb, None);
|
assert_eq!(open_abb, None);
|
||||||
open_abb_at_this_task_or_level.insert((trace[i].level, if trace[i].level<2 {&curr_name} else {""}), i);
|
open_abb_at_this_task_or_level.insert((trace[i].level, if trace[i].level<2 {&curr_name} else {""}), i);
|
||||||
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
||||||
},
|
},
|
||||||
// generic isr abb start
|
// generic isr abb start
|
||||||
CaptureEvent::ISRStart => {
|
CaptureEvent::ISRStart => {
|
||||||
// assert_eq!(open_abb, None);
|
assert_eq!(open_abb, None);
|
||||||
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}) , i);
|
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {""}) , i);
|
||||||
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
||||||
},
|
},
|
||||||
// generic app abb start
|
// generic app abb start
|
||||||
CaptureEvent::APIEnd => {
|
CaptureEvent::APIEnd => {
|
||||||
// assert_eq!(open_abb, None);
|
assert_eq!(open_abb, None);
|
||||||
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}) , i);
|
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {""}) , i);
|
||||||
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].0.unwrap(), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
||||||
},
|
},
|
||||||
// generic continued blocks
|
// generic continued blocks
|
||||||
CaptureEvent::ISREnd => {
|
CaptureEvent::ISREnd => {
|
||||||
// special case app abb start
|
// special case app abb start
|
||||||
if trace[i].start_capture.1=="xPortPendSVHandler" && !task_has_started.contains(curr_name) {
|
if trace[i].start_capture.1=="xPortPendSVHandler" && !task_has_started.contains(curr_name) {
|
||||||
// assert_eq!(open_abb, None);
|
assert_eq!(open_abb, None);
|
||||||
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: 0, ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: 0, ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})));
|
||||||
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}) , i);
|
open_abb_at_this_task_or_level.insert( (trace[i].level, if trace[i].level<2 {&curr_name} else {""}) , i);
|
||||||
task_has_started.insert(curr_name.clone());
|
task_has_started.insert(curr_name.clone());
|
||||||
} else {
|
} else {
|
||||||
if let Some(last) = open_abb_at_this_task_or_level.get(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()})) {
|
// assert_ne!(open_abb,None);
|
||||||
|
if let Some(last) = open_abb_at_this_task_or_level.get(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""})) {
|
||||||
wip_abb_trace.push(wip_abb_trace[*last].clone());
|
wip_abb_trace.push(wip_abb_trace[*last].clone());
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Continued block with no start {} {:?} {:?} {} {}", trace[i].start_tick, trace[i].start_capture, trace[i].end_capture, task_has_started.contains(curr_name),trace[i].level);
|
// panic!();
|
||||||
|
eprintln!("Continued block with no start {} {:?} {:?} {:x}-{:x} {} {}", trace[i].start_tick, trace[i].start_capture, trace[i].end_capture, edges[i].0.unwrap_or(0xffff), edges[i].1.unwrap_or(0xffff),task_has_started.contains(curr_name),trace[i].level);
|
||||||
|
panic!();
|
||||||
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].1.unwrap_or(0), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})))
|
wip_abb_trace.push(Rc::new(RefCell::new(AtomicBasicBlock{start: edges[i].1.unwrap_or(0), ends: HashSet::new(), level: if trace[i].level<2 {trace[i].level} else {2}})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,29 +315,29 @@ fn add_abb_info(trace: &mut Vec<ExecInterval>, table: &HashMap<u64, ReducedFreeR
|
|||||||
CaptureEvent::APIStart => {
|
CaptureEvent::APIStart => {
|
||||||
let _t = &wip_abb_trace[i];
|
let _t = &wip_abb_trace[i];
|
||||||
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
||||||
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}));
|
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""}));
|
||||||
},
|
},
|
||||||
// generic api abb end
|
// generic api abb end
|
||||||
CaptureEvent::APIEnd => {
|
CaptureEvent::APIEnd => {
|
||||||
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
||||||
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}));
|
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""}));
|
||||||
},
|
},
|
||||||
// generic isr abb end
|
// generic isr abb end
|
||||||
CaptureEvent::ISREnd => {
|
CaptureEvent::ISREnd => {
|
||||||
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
||||||
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}));
|
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""}));
|
||||||
},
|
},
|
||||||
// end anything
|
// end anything
|
||||||
CaptureEvent::End => {
|
CaptureEvent::End => {
|
||||||
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
RefCell::borrow_mut(&*wip_abb_trace[i]).ends.insert(edges[i].1.unwrap());
|
||||||
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {trace[i].start_capture.1.as_str()}));
|
open_abb_at_this_task_or_level.remove(&(trace[i].level, if trace[i].level<2 {&curr_name} else {""}));
|
||||||
},
|
},
|
||||||
CaptureEvent::ISRStart => (),
|
CaptureEvent::ISRStart => (),
|
||||||
_ => panic!("Undefined block end")
|
_ => panic!("Undefined block end")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// println!("{} {} {:x}-{:x} {:x}-{:X} {:?} {:?} {}",curr_name, trace[i].level, edges[i].0.unwrap_or(0xffff), edges[i].1.unwrap_or(0xffff), ((*wip_abb_trace[i])).borrow().start, ((*wip_abb_trace[i])).borrow().ends.iter().next().unwrap_or(&0xffff), trace[i].start_capture, trace[i].end_capture, trace[i].start_tick);
|
// println!("{} {} {:x}-{:x} {:x}-{:x} {:?} {:?} {}",curr_name, trace[i].level, edges[i].0.unwrap_or(0xffff), edges[i].1.unwrap_or(0xffff), ((*wip_abb_trace[i])).borrow().start, ((*wip_abb_trace[i])).borrow().ends.iter().next().unwrap_or(&0xffff), trace[i].start_capture, trace[i].end_capture, trace[i].start_tick);
|
||||||
}
|
}
|
||||||
drop(open_abb_at_this_task_or_level);
|
drop(open_abb_at_this_task_or_level);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user