Repository: binarly-io/idalib (language: rust)

Issue:
Extended error information for `decompile_func()` not available
At the moment, `decompile_func()` returns an `Option<cxx::UniquePtr<cfuncptr_t>>`. This means that if the decompiler is generally available (i.e., `decompiler_available()` is `true`), but the license in use doesn't include a suitable decompiler for the current binary file, we would get a `None` result with no indication of the reason for the failure.

For instance, in [haruspex](https://github.com/0xdea/haruspex) I'm forced to do something like the following to catch this type of failure:
```rust
    // Remove output directory and return an error in case no functions were decompiled
    if COUNTER.load(Ordering::Relaxed) == 0 {
        fs::remove_dir(&dirpath)?;
        return Err(anyhow::anyhow!(
            "no functions were decompiled, check your license and input file"
        ));
    }
```

It would be convenient to be able to get extended error information (see `hexrays_failure_t` and `merror_t` in `include/hexrays.hpp` in the IDA SDK) to catch "no license available" and other error cases. Thanks!


Discussion / hints:
That’s not ideal at all :). I’ll see about updating the API to report errors rather than silently failing. 

Fix patch:
diff --git a/idalib-sys/src/hexrays_extras.h b/idalib-sys/src/hexrays_extras.h
index 605400e50..f0223155a 100644
--- a/idalib-sys/src/hexrays_extras.h
+++ b/idalib-sys/src/hexrays_extras.h
@@ -10,6 +10,17 @@
 
 #include "cxx.h"
 
+#ifndef CXXBRIDGE1_STRUCT_hexrays_error_t
+#define CXXBRIDGE1_STRUCT_hexrays_error_t
+struct hexrays_error_t final {
+  ::std::int32_t code;
+  ::std::uint64_t addr;
+  ::rust::String desc;
+
+  using IsRelocatable = ::std::true_type;
+};
+#endif // CXXBRIDGE1_STRUCT_hexrays_error_t
+
 struct cblock_iter {
   qlist<cinsn_t>::iterator start;
   qlist<cinsn_t>::iterator end;
@@ -19,6 +30,22 @@ struct cblock_iter {
 
 cfunc_t *idalib_hexrays_cfuncptr_inner(const cfuncptr_t *f) { return *f; }
 
+std::unique_ptr<cfuncptr_t>
+idalib_hexrays_decompile_func(func_t *f, hexrays_error_t *err, int flags) {
+  hexrays_failure_t failure;
+  cfuncptr_t cf = decompile_func(f, &failure, flags);
+
+  if (failure.code >= 0 && cf != nullptr) {
+    return std::unique_ptr<cfuncptr_t>(new cfuncptr_t(cf));
+  }
+
+  err->code = failure.code;
+  err->desc = rust::String(failure.desc().c_str());
+  err->addr = failure.errea;
+
+  return nullptr;
+}
+
 rust::String idalib_hexrays_cfunc_pseudocode(cfunc_t *f) {
   auto sv = f->get_pseudocode();
   auto sb = std::stringstream();
@@ -44,6 +71,4 @@ cinsn_t *idalib_hexrays_cblock_iter_next(cblock_iter &it) {
   return nullptr;
 }
 
-std::size_t idalib_hexrays_cblock_len(cblock_t *b) {
-  return b->size();
-}
+std::size_t idalib_hexrays_cblock_len(cblock_t *b) { return b->size(); }
diff --git a/idalib-sys/src/lib.rs b/idalib-sys/src/lib.rs
index f4f07e9ff..68d7b60b2 100644
--- a/idalib-sys/src/lib.rs
+++ b/idalib-sys/src/lib.rs
@@ -12,6 +12,8 @@ mod platform;
 pub enum IDAError {
     #[error(transparent)]
     Ffi(anyhow::Error),
+    #[error(transparent)]
+    HexRays(#[from] hexrays::HexRaysError),
     #[error("could not initialise IDA: error code {:x}", _0.0)]
     Init(c_int),
     #[error("could not open IDA database: error code {:x}", _0.0)]
@@ -49,6 +51,7 @@ include_cpp! {
     #include "entry.hpp"
     #include "funcs.hpp"
     #include "gdl.hpp"
+    #include "hexrays.hpp"
     #include "ida.hpp"
     #include "idalib.hpp"
     #include "idp.hpp"
@@ -58,8 +61,6 @@ include_cpp! {
     #include "segment.hpp"
     #include "ua.hpp"
     #include "xref.hpp"
-    #include "hexrays.hpp"
-
 
     generate!("qstring")
 
@@ -147,10 +148,53 @@ include_cpp! {
     generate!("init_hexrays_plugin")
     generate!("term_hexrays_plugin")
 
-    generate!("decompile_func")
+    // generate!("decompile_func")
     generate!("cfuncptr_t")
     generate!("hexrays_failure_t")
 
+    generate_pod!("merror_t")
+
+    /*
+    generate!("MERR_OK")
+    generate!("MERR_BLOCK")
+    generate!("MERR_INTERR")
+    generate!("MERR_INSN")
+    generate!("MERR_MEM")
+    generate!("MERR_BADBLK")
+    generate!("MERR_BADSP")
+    generate!("MERR_PROLOG")
+    generate!("MERR_SWITCH")
+    generate!("MERR_EXCEPTION")
+    generate!("MERR_HUGESTACK")
+    generate!("MERR_LVARS")
+    generate!("MERR_BITNESS")
+    generate!("MERR_BADCALL")
+    generate!("MERR_BADFRAME")
+    generate!("MERR_UNKTYPE")
+    generate!("MERR_BADIDB")
+    generate!("MERR_SIZEOF")
+    generate!("MERR_REDO")
+    generate!("MERR_CANCELED")
+    generate!("MERR_RECDEPTH")
+    generate!("MERR_OVERLAP")
+    generate!("MERR_PARTINIT")
+    generate!("MERR_COMPLEX")
+    generate!("MERR_LICENSE")
+    generate!("MERR_ONLY")
+    generate!("MERR_ONLY")
+    generate!("MERR_BUSY")
+    generate!("MERR_FARPTR")
+    generate!("MERR_EXTERN")
+    generate!("MERR_FUNCSIZE")
+    generate!("MERR_BADRANGES")
+    generate!("MERR_BADARCH")
+    generate!("MERR_DSLOT")
+    generate!("MERR_STOP")
+    generate!("MERR_CLOUD")
+    generate!("MERR_MAX_ERR")
+    generate!("MERR_LOOP")
+    */
+
     generate!("carg_t")
     generate!("carglist_t")
 
@@ -304,6 +348,133 @@ include_cpp! {
 }
 
 pub mod hexrays {
+    use std::mem;
+
+    use thiserror::Error;
+
+    // NOTE: we don't export it; ideally this conversion should exist in idalib (not -sys), but it
+    // having the conversion here gives us a cleaner interface.
+    use super::ffi::merror_t;
+
+    #[derive(Debug, Error)]
+    #[error("{desc}")]
+    pub struct HexRaysError {
+        code: HexRaysErrorCode,
+        addr: u64,
+        desc: String,
+    }
+
+    impl HexRaysError {
+        pub fn code(&self) -> HexRaysErrorCode {
+            self.code
+        }
+
+        pub fn address(&self) -> u64 {
+            self.addr
+        }
+
+        pub fn reason(&self) -> &str {
+            &self.desc
+        }
+    }
+
+    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+    pub enum HexRaysErrorCode {
+        Ok,
+        Block,
+        Internal,
+        Insn,
+        Mem,
+        BadBlock,
+        BadSp,
+        Prolog,
+        Switch,
+        Exception,
+        HugeStack,
+        LVars,
+        Bitness,
+        BadCall,
+        BadFrame,
+        UnknownType,
+        BadIDB,
+        SizeOf,
+        Redo,
+        Cancelled,
+        RecursionDepth,
+        Overlap,
+        PartInitVar,
+        Complex,
+        License,
+        Only32,
+        Only64,
+        Busy,
+        FarPtr,
+        Extern,
+        FuncSize,
+        BadRanges,
+        BadArch,
+        DelaySlot,
+        Stop,
+        Cloud,
+        Loop,
+        Unknown,
+    }
+
+    impl HexRaysErrorCode {
+        pub fn is_ok(&self) -> bool {
+            matches!(self, Self::Ok | Self::Block)
+        }
+
+        pub fn is_err(&self) -> bool {
+            !self.is_ok()
+        }
+    }
+
+    impl From<merror_t> for HexRaysErrorCode {
+        fn from(value: merror_t) -> Self {
+            match value {
+                merror_t::MERR_OK => Self::Ok,
+                merror_t::MERR_BLOCK => Self::Block,
+                merror_t::MERR_INTERR => Self::Internal,
+                merror_t::MERR_INSN => Self::Insn,
+                merror_t::MERR_MEM => Self::Mem,
+                merror_t::MERR_BADBLK => Self::BadBlock,
+                merror_t::MERR_BADSP => Self::BadSp,
+                merror_t::MERR_PROLOG => Self::Prolog,
+                merror_t::MERR_SWITCH => Self::Switch,
+                merror_t::MERR_EXCEPTION => Self::Exception,
+                merror_t::MERR_HUGESTACK => Self::HugeStack,
+                merror_t::MERR_LVARS => Self::LVars,
+                merror_t::MERR_BITNESS => Self::Bitness,
+                merror_t::MERR_BADCALL => Self::BadCall,
+                merror_t::MERR_BADFRAME => Self::BadFrame,
+                merror_t::MERR_UNKTYPE => Self::UnknownType,
+                merror_t::MERR_BADIDB => Self::BadIDB,
+                merror_t::MERR_SIZEOF => Self::SizeOf,
+                merror_t::MERR_REDO => Self::Redo,
+                merror_t::MERR_CANCELED => Self::Cancelled,
+                merror_t::MERR_RECDEPTH => Self::RecursionDepth,
+                merror_t::MERR_OVERLAP => Self::Overlap,
+                merror_t::MERR_PARTINIT => Self::PartInitVar,
+                merror_t::MERR_COMPLEX => Self::Complex,
+                merror_t::MERR_LICENSE => Self::License,
+                merror_t::MERR_ONLY32 => Self::Only32,
+                merror_t::MERR_ONLY64 => Self::Only64,
+                merror_t::MERR_BUSY => Self::Busy,
+                merror_t::MERR_FARPTR => Self::FarPtr,
+                merror_t::MERR_EXTERN => Self::Extern,
+                merror_t::MERR_FUNCSIZE => Self::FuncSize,
+                merror_t::MERR_BADRANGES => Self::BadRanges,
+                merror_t::MERR_BADARCH => Self::BadArch,
+                merror_t::MERR_DSLOT => Self::DelaySlot,
+                merror_t::MERR_STOP => Self::Stop,
+                merror_t::MERR_CLOUD => Self::Cloud,
+                merror_t::MERR_LOOP => Self::Loop,
+                _ => Self::Unknown,
+            }
+        }
+    }
+
     mod __impl {
         #![allow(non_camel_case_types)]
         #![allow(non_upper_case_globals)]
@@ -321,6 +492,7 @@ pub mod hexrays {
     pub use super::ffix::{
         cblock_iter, idalib_hexrays_cblock_iter, idalib_hexrays_cblock_iter_next,
         idalib_hexrays_cblock_len, idalib_hexrays_cfunc_pseudocode, idalib_hexrays_cfuncptr_inner,
+        idalib_hexrays_decompile_func,
     };
 
     unsafe impl cxx::ExternType for cfunc_t {
@@ -366,20 +538,31 @@ pub mod hexrays {
     pub unsafe fn decompile_func(
         f: *mut super::ffi::func_t,
         all_blocks: bool,
-    ) -> Option<cxx::UniquePtr<cfuncptr_t>> {
+    ) -> Result<cxx::UniquePtr<cfuncptr_t>, HexRaysError> {
         let mut flags = __impl::DECOMP_NO_WAIT | __impl::DECOMP_NO_CACHE;
 
         if all_blocks {
             flags |= __impl::DECOMP_ALL_BLKS;
         }
 
-        let result = super::ffi::decompile_func(f, std::ptr::null_mut(), (flags as i32).into());
+        let mut failure = super::ffix::hexrays_error_t::default();
+        let result = super::ffix::idalib_hexrays_decompile_func(
+            f,
+            &mut failure as *mut _,
+            (flags as i32).into(),
+        );
 
-        if result.is_null() {
-            return None;
-        }
+        let code = HexRaysErrorCode::from(mem::transmute::<i32, merror_t>(failure.code));
 
-        Some(result)
+        if result.is_null() || code.is_err() {
+            Err(HexRaysError {
+                addr: failure.addr,
+                code,
+                desc: failure.desc,
+            })
+        } else {
+            Ok(result)
+        }
     }
 }
 
@@ -488,6 +671,13 @@ pub mod pod {
 
 #[cxx::bridge]
 mod ffix {
+    #[derive(Default)]
+    struct hexrays_error_t {
+        code: i32,
+        addr: u64,
+        desc: String,
+    }
+
     unsafe extern "C++" {
         include!("autocxxgen_ffi.h");
         include!("idalib.hpp");
@@ -558,6 +748,12 @@ mod ffix {
         ) -> *mut cfunc_t;
         unsafe fn idalib_hexrays_cfunc_pseudocode(f: *mut cfunc_t) -> String;
 
+        unsafe fn idalib_hexrays_decompile_func(
+            f: *mut func_t,
+            err: *mut hexrays_error_t,
+            flags: c_int,
+        ) -> UniquePtr<qrefcnt_t_cfunc_t_AutocxxConcrete>;
+
         unsafe fn idalib_hexrays_cblock_iter(b: *mut cblock_t) -> UniquePtr<cblock_iter>;
         unsafe fn idalib_hexrays_cblock_iter_next(slf: Pin<&mut cblock_iter>) -> *mut cinsn_t;
         unsafe fn idalib_hexrays_cblock_len(b: *mut cblock_t) -> usize;
diff --git a/idalib/examples/dump_ls.rs b/idalib/examples/dump_ls.rs
index 1c9e048b7..7723bbf09 100644
--- a/idalib/examples/dump_ls.rs
+++ b/idalib/examples/dump_ls.rs
@@ -1,5 +1,6 @@
 use idalib::func::FunctionFlags;
 use idalib::idb::*;
+use idalib::IDAError;
 
 fn main() -> anyhow::Result<()> {
     let idb = IDB::open("./tests/ls")?;
@@ -53,16 +54,22 @@ fn main() -> anyhow::Result<()> {
             "function {fid} @ {addr:x}: {:?} (blocks: {}) (decompiled: {})",
             f.name(),
             fcfg.blocks_count(),
-            decompiled.is_some(),
+            decompiled.is_ok(),
         );
 
-        if let Some(df) = decompiled.as_ref() {
-            println!(
-                "statements: {} / {}",
-                df.body().len(),
-                df.body().iter().count()
-            );
-            println!("{}", df.pseudocode());
+        match decompiled.as_ref() {
+            Ok(df) => {
+                println!(
+                    "statements: {} / {}",
+                    df.body().len(),
+                    df.body().iter().count()
+                );
+                println!("{}", df.pseudocode());
+            }
+            Err(IDAError::HexRays(e)) => {
+                println!("decompilation failed: {:?} / {}", e.code(), e.reason());
+            }
+            _ => (),
         }
 
         for (id, blk) in fcfg.blocks().enumerate() {
diff --git a/idalib/src/decompiler.rs b/idalib/src/decompiler.rs
index 4e59db472..a910f18b2 100644
--- a/idalib/src/decompiler.rs
+++ b/idalib/src/decompiler.rs
@@ -7,6 +7,8 @@ use crate::ffi::hexrays::{
 };
 use crate::idb::IDB;
 
+pub use crate::ffi::hexrays::{HexRaysError, HexRaysErrorCode};
+
 pub struct CFunction<'a> {
     ptr: *mut cfunc_t,
     _obj: cxx::UniquePtr<cfuncptr_t>,
diff --git a/idalib/src/idb.rs b/idalib/src/idb.rs
index 3ea715014..d196327ae 100644
--- a/idalib/src/idb.rs
+++ b/idalib/src/idb.rs
@@ -152,7 +152,7 @@ impl IDB {
         Some(Insn::from_repr(insn))
     }
 
-    pub fn decompile<'a>(&'a self, f: &Function<'a>) -> Option<CFunction<'a>> {
+    pub fn decompile<'a>(&'a self, f: &Function<'a>) -> Result<CFunction<'a>, IDAError> {
         self.decompile_with(f, false)
     }
 
@@ -160,12 +160,15 @@ impl IDB {
         &'a self,
         f: &Function<'a>,
         all_blocks: bool,
-    ) -> Option<CFunction<'a>> {
+    ) -> Result<CFunction<'a>, IDAError> {
         if !self.decompiler {
-            return None;
+            return Err(IDAError::ffi_with("no decompiler available"));
         }
 
-        unsafe { decompile_func(f.as_ptr(), all_blocks).and_then(CFunction::new) }
+        Ok(unsafe {
+            decompile_func(f.as_ptr(), all_blocks)
+                .map(|f| CFunction::new(f).expect("null pointer checked"))?
+        })
     }
 
     pub fn function_by_id(&self, id: FunctionId) -> Option<Function> {


Test patch: