Unexpected unused variable when analyzing x86_64 Rust binary
Version and Platform (required):
- Binary Ninja Version: 5.2.8574-dev, b85f6a6
- OS: windows
- OS Version: 11
- CPU Architecture: x86_64
Bug Description: Binja wrongly marks a local variable as dead code :
The instruction at 0x415877 is displayed as dead code when it's actually used.
My assumption is that binja's HLIL/MLIL liveness doesn't see the indirect use through a pointer, so it (wrongly) marks the local var_40_1 as dead
The source code is
fn main() {
if check() {
println!("{}", log_headers());
}
}
Which desugars roughly to
if check() {
let tmp = log_headers();
std::io::_print(format_args!("{}", tmp));
}
format_args! builds a core::fmt::Arguments struct, which internally contains an array of ArgumentV1 objects where each ArgumentV1 is basically:
struct ArgumentV1<'a> {
data: *const ();// pointer to the value
fmt: fn(*const (), &mut Formatter), // formatting function (Display/Debug/etc)
}
If you look at the disasm:
You can see that var_48 in the HLIL is &String (which we can confirm by looking into test1::log_headers) and var_40_1 is a fmt fn. _print uses rdi (pointing to var_38), which in turn contains that args slice pointing to var_48. Inside _print, it will dereference that slice, read both fields of the ArgumentV1, and call the formatter function pointer (the one stored at 0x415877 in the screenshot)
From Binja's perspective, var_40_1 is never read, only &var_48 is passed onward. And binja doesn't know that the callee will load 16 bytes from that address and use the second qword as a function pointer. Thus, the liveness / dead-store analysis says "var_40_1 is assigned but never used → dead store"
This is basically an analysis limitation (arguably a bug?) in how Binja handles aggregates/structs and unknown function calls in HLIL/MLIL, especially for Rust's fat formatting stuff
Steps To Reproduce:
Open the binary, go to test1::main::h30c3c123818d97db
Expected Behavior: Instruction at 0x00415877 shouldn't appear as dead code, and should in a way or another display that it's part of a struct. IDA displays the following:
_BYTE v1[24]; // [rsp+8h] [rbp-60h] BYREF
_QWORD v2[2]; // [rsp+20h] [rbp-48h] BYREF
_QWORD v3[7]; // [rsp+30h] [rbp-38h] BYREF
// ... snipped for clarity ...
test1::log_headers(v1);
v2[0] = v1;
v2[1] = <alloc::string::String as core::fmt::Display>::fmt;
v3[0] = &unk_56CC8;
v3[1] = 2LL;
v3[4] = 0LL;
v3[2] = v2;
v3[3] = 1LL;
std::io::stdio::_print(v3);
<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop(v1);
return <alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop(v1);
which looks like a more proper way to display it?
It would be great if Binja could do some amount of stack aggregate recovery here, or at least be more conservative about marking stores as dead when:
- a contiguous range of stack slots is initialized together, and
- the address of the first slot is taken (
lea reg, [rsp+K]) and passed to a call
Binary: Attached: test1.zip
Source is :
#![deny(unsafe_op_in_unsafe_fn)]
use std::fmt::Write as _;
const REQ_LINE: &str = "GET /health HTTP/1.1\r\n";
const HDR_HOST_LINE: &str = "Host: api.service.local\r\n";
const HDR_UA_LINE: &str = "User-Agent: audit/1.0\r\n";
const HDR_ACC_LINE: &str = "Accept: application/json\r\n";
pub static POOL: &str = "GET /health HTTP/1.1\r\n\
Host: api.service.local\r\n\
User-Agent: audit/1.0\r\n\
Accept: application/json\r\n";
const REQ_START: usize = 0;
const REQ_END: usize = REQ_START + REQ_LINE.len();
const HOST_START: usize = REQ_END;
const HOST_END: usize = HOST_START + HDR_HOST_LINE.len();
const UA_START: usize = HOST_END;
const UA_END: usize = UA_START + HDR_UA_LINE.len();
const ACC_START: usize = UA_END;
const ACC_END: usize = ACC_START + HDR_ACC_LINE.len();
#[inline(always)]
pub fn S_REQ_LINE() -> &'static str {
&POOL[REQ_START..REQ_END]
}
#[inline(always)]
pub fn S_HOST() -> &'static str {
&POOL[HOST_START..HOST_END]
}
#[inline(always)]
pub fn S_UA() -> &'static str {
&POOL[UA_START..UA_END]
}
#[inline(always)]
pub fn S_ACC() -> &'static str {
&POOL[ACC_START..ACC_END]
}
#[inline(never)]
fn log_headers() -> String {
let mut out = String::new();
let _ = write!(&mut out, "{}{}{}", S_HOST(), S_UA(), S_ACC());
out
}
#[inline(never)]
fn check() -> bool {
S_REQ_LINE().starts_with("GET ")
&& S_ACC().ends_with("\r\n")
&& matches!(S_HOST(), "Host: api.service.local\r\n")
}
fn main() {
if check() {
println!("{}", log_headers());
}
}