Skip to content

Commit 86d962d

Browse files
authored
Merge pull request #83 from dword64/master
Fix tracer recursion stack overflows (#52, #78) by combining #79/#81; update Capstone/Keystone deps
2 parents 6f21abb + 4292245 commit 86d962d

7 files changed

Lines changed: 169 additions & 11 deletions

File tree

Dependencies/capstone/CMakeLists.txt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ message(STATUS "Fetching capstone (this might take a while)...")
1111
FetchContent_Declare(
1212
capstone
1313
GIT_REPOSITORY https://github.com/aquynh/capstone
14-
GIT_TAG d71c95b09b1fa478c0ab9294b07fd6f2efaa1b31
14+
GIT_TAG 52c66920fc7bfa15fd9626dfd9f646c698aaa99b
1515
GIT_SHALLOW false
1616
)
1717
FetchContent_MakeAvailable(capstone)
@@ -22,9 +22,27 @@ FetchContent_MakeAvailable(capstone)
2222
#
2323
# TODO: Maybe contribute a fix to these upstream projects?
2424
#
25-
get_target_property(capstone_SOURCE_DIR capstone-static SOURCE_DIR)
26-
set_property(TARGET capstone-static PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${capstone_SOURCE_DIR}/include)
25+
# capstone target naming differs by version/toolchain; resolve the first valid one cus lazy
26+
set(CAPSTONE_TARGET "")
27+
foreach(_candidate capstone-static capstone_static capstone)
28+
if(TARGET ${_candidate})
29+
set(CAPSTONE_TARGET ${_candidate})
30+
break()
31+
endif()
32+
endforeach()
33+
34+
if(NOT CAPSTONE_TARGET)
35+
message(FATAL_ERROR "Capstone target was not created (checked: capstone-static, capstone_static, capstone)")
36+
endif()
37+
38+
get_target_property(capstone_SOURCE_DIR ${CAPSTONE_TARGET} SOURCE_DIR)
39+
set_property(TARGET ${CAPSTONE_TARGET} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${capstone_SOURCE_DIR}/include)
2740

2841
# Downgrade C++ standard on these targets since they depend on some removed/deprecated features
29-
set_property(TARGET capstone-static PROPERTY CXX_STANDARD 11)
30-
set_property(TARGET capstone-static PROPERTY CXX_STANDARD_REQUIRED ON)
42+
set_property(TARGET ${CAPSTONE_TARGET} PROPERTY CXX_STANDARD 11)
43+
set_property(TARGET ${CAPSTONE_TARGET} PROPERTY CXX_STANDARD_REQUIRED ON)
44+
45+
# Keep downstream link line stable across Capstone naming variants.
46+
if(NOT TARGET capstone-static)
47+
add_library(capstone-static ALIAS ${CAPSTONE_TARGET})
48+
endif()

Dependencies/keystone/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ message(STATUS "Fetching keystone (this might take a while)...")
22
FetchContent_Declare(
33
keystone
44
GIT_REPOSITORY https://github.com/keystone-engine/keystone
5-
GIT_TAG e1547852d9accb9460573eb156fc81645b8e1871
5+
GIT_TAG dc7932ef2b2c4a793836caec6ecab485005139d6
66
GIT_SHALLOW false
77
)
88

VTIL-Architecture/trace/tracer.cpp

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,39 @@
2929
#include <vtil/io>
3030
#include "../vm/lambda.hpp"
3131
#include <vtil/utility>
32+
#include <unordered_map>
3233

3334
namespace vtil
3435
{
36+
// Re entry guards for cycle heavy traces.
37+
// - #52: direct/indirect reentry in tracer::trace via VM callbacks.
38+
// - #78: recursive rtrace propagation over cyclic path expansions.
39+
//
40+
template<typename key_t>
41+
struct recursion_guard
42+
{
43+
std::unordered_map<key_t, size_t>& depth_map;
44+
key_t key;
45+
bool reentrant = false;
46+
47+
recursion_guard( std::unordered_map<key_t, size_t>& depth_map, const key_t& key )
48+
: depth_map( depth_map ), key( key )
49+
{
50+
auto& depth = depth_map[ key ];
51+
reentrant = depth != 0;
52+
depth++;
53+
}
54+
55+
~recursion_guard()
56+
{
57+
auto it = depth_map.find( key );
58+
if ( it != depth_map.end() && --it->second == 0 )
59+
depth_map.erase( it );
60+
}
61+
};
62+
63+
inline static thread_local std::unordered_map<symbolic::variable, size_t> active_trace;
64+
inline static thread_local std::unordered_map<symbolic::variable, size_t> active_rtrace;
3565
// Internal type definitions.
3666
//
3767
using path_map_t = std::map<std::pair<const basic_block*, const basic_block*>, int>;
@@ -284,6 +314,14 @@ namespace vtil
284314
{
285315
using namespace logger;
286316

317+
recursion_guard guard_rtrace{ active_rtrace, lookup };
318+
if ( guard_rtrace.reentrant )
319+
{
320+
auto cyclic = lookup;
321+
cyclic.is_branch_dependant = true;
322+
return cyclic.to_expression();
323+
}
324+
287325
// Save whether this is the call whose result will reach the user.
288326
//
289327
bool initial_call = path_map.empty();
@@ -355,9 +393,9 @@ namespace vtil
355393
if ( counter >= 2 )
356394
{
357395
#if VTIL_OPT_TRACE_VERBOSE
358-
// Log skipping of path.
359-
//
360-
log<CON_CYN>( "Path [%llx->%llx] is not taken as it's n-looping.\n", lookup.at.block->entry_vip, it.block->entry_vip );
396+
// Log skipping of path.
397+
//
398+
log<CON_CYN>( "Path [%llx->%llx] is not taken as it's n-looping.\n", lookup.at.block->entry_vip, it.block->entry_vip );
361399
#endif
362400
return enumerator::ocontinue;
363401
}
@@ -446,6 +484,10 @@ namespace vtil
446484
{
447485
using namespace logger;
448486

487+
recursion_guard guard_trace{ active_trace, lookup };
488+
if ( guard_trace.reentrant )
489+
return lookup.to_expression();
490+
449491
// If invalid/.begin() iterator or register with "no-trace" flags, return as is.
450492
//
451493
if ( lookup.at.is_begin() || ( lookup.is_register() && ( lookup.reg().flags & ( register_volatile | register_readonly ) ) ) )

VTIL-Common/util/copy_on_write.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <functional>
3131
#include <type_traits>
3232
#include <atomic>
33+
#include <utility>
3334
#include "../io/asserts.hpp"
3435
#include "object_pool.hpp"
3536
#include "../util/intrinsics.hpp"

VTIL-Common/util/task.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ namespace vtil::task
148148
#if VTIL_USE_THREAD_POOLING
149149
handle = std::async( std::launch::async, std::move( f ) );
150150
#else
151-
handle = { f };
151+
handle = std::thread( std::move( f ) );
152152
#endif
153153
}
154154

@@ -171,4 +171,4 @@ namespace vtil::task
171171
#endif
172172
}
173173
};
174-
};
174+
};

VTIL-Compiler/optimizer/register_renaming_pass.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ namespace vtil::optimizer
111111
//
112112
if ( i->is_volatile() )
113113
return fail();
114+
115+
// If source is being accessed by a vxcall instruction, fail.
116+
//
117+
if ( mask && i->base == &ins::vxcall )
118+
return fail();
114119
}
115120

116121
// If destination is used by the instruction, fail.

VTIL-Tests/dummy.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,58 @@ namespace registers
1919
}
2020

2121

22+
DOCTEST_TEST_CASE("Tracer stack overflow protection (#52 VM callback reentry)")
23+
{
24+
vtil::logger::log("\n\n>> %s \n", __FUNCTION__);
25+
26+
// Create a scenario where trace -> trace (via VM hooks) could recurse.
27+
// This simulates the #52 pattern where read_register calls back into trace.
28+
auto block = vtil::basic_block::begin(0x1000);
29+
vtil::register_desc reg_ax(vtil::register_physical, registers::ax, vtil::arch::bit_count, 0);
30+
31+
// mov eax, eax (self-reference that would cause reentry during VM execution)
32+
block->mov(reg_ax, reg_ax);
33+
block->vexit(0ull);
34+
35+
// Trace the register at the end - should not stack overflow
36+
vtil::tracer tracer;
37+
auto result = tracer.trace({ std::prev(block->end()), reg_ax });
38+
39+
// Result should be valid and stable (either original reg or simplified constant)
40+
CHECK(result.get() != nullptr);
41+
vtil::logger::log(" Trace result: %s\n", result->to_string().c_str());
42+
}
43+
44+
DOCTEST_TEST_CASE("Tracer stack overflow protection (#78 cyclic path propagation)")
45+
{
46+
vtil::logger::log("\n\n>> %s \n", __FUNCTION__);
47+
48+
// Create a CFG with back edge to trigger #78 pattern:
49+
// Block1 -> Block2 -> Block1 (cycle)
50+
// Trace should terminate safely when encountering symbolic cycles.
51+
auto block1 = vtil::basic_block::begin(0x1000);
52+
vtil::register_desc reg_ax(vtil::register_physical, registers::ax, vtil::arch::bit_count, 0);
53+
54+
block1->mov(reg_ax, 0x100);
55+
block1->js(vtil::REG_FLAGS, 0x2000ull, 0x3000ull);
56+
57+
auto block2 = block1->fork(0x2000);
58+
block2->add(reg_ax, 1);
59+
block2->jmp(0x1000ull); // Back edge to block1
60+
block2->fork(0x1000ull);
61+
62+
auto block3 = block1->fork(0x3000);
63+
block3->vexit(0ull);
64+
65+
// rtrace should terminate without stack overflow despite the cycle
66+
vtil::tracer tracer;
67+
auto result = tracer.rtrace({ block3->begin(), reg_ax });
68+
69+
// Result should be valid (either concrete or branch ddependent)
70+
CHECK(result.get() != nullptr);
71+
vtil::logger::log(" RTrace result: %s\n", result->to_string().c_str());
72+
}
73+
2274
DOCTEST_TEST_CASE("dummy")
2375
{
2476
vtil::logger::log("\n\n>> %s \n", __FUNCTION__);
@@ -285,6 +337,46 @@ DOCTEST_TEST_CASE("Optimization register_renaming_pass")
285337
CHECK(ins.operands[1].imm().ival == 0x1);
286338
}
287339

340+
DOCTEST_TEST_CASE("Optimization register_renaming_pass vxcall")
341+
{
342+
vtil::logger::log("\n\n>> %s \n", __FUNCTION__);
343+
344+
auto block = vtil::basic_block::begin(0x1337);
345+
346+
vtil::register_desc reg_ecx(vtil::register_physical, registers::cx, vtil::arch::bit_count, 0);
347+
348+
auto sr0 = block->owner->alloc(vtil::arch::bit_count);
349+
350+
// The ecx register here is a potential function argument, register_renaming_pass should not work here.
351+
block->mov(reg_ecx, (uintptr_t)0x880000);
352+
block->vxcall((uintptr_t)0x10000);
353+
354+
auto block2 = block->fork(0x2000);
355+
block2->mov(sr0, reg_ecx);
356+
block2->mov(reg_ecx, (uintptr_t)1);
357+
block2->mov(reg_ecx, sr0);
358+
block2->vxcall((uintptr_t)0x10000);
359+
360+
auto block3 = block2->fork(0x3000);
361+
block3->vexit(0ull); // marks the end of a basic_block
362+
363+
vtil::logger::log(":: Before:\n");
364+
vtil::debug::dump(block->owner);
365+
366+
vtil::optimizer::register_renaming_pass{}(block->owner);
367+
368+
vtil::logger::log(":: After:\n");
369+
vtil::debug::dump(block->owner);
370+
371+
auto ins = (*block)[0];
372+
373+
// mov ecx, 0x880000
374+
CHECK(ins.base == &vtil::ins::mov);
375+
CHECK(ins.operands.size() == 2);
376+
CHECK(ins.operands[0].reg().local_id == registers::cx);
377+
CHECK(ins.operands[1].imm().ival == 0x880000);
378+
}
379+
288380
DOCTEST_TEST_CASE("Optimization symbolic_rewrite_pass<true>")
289381
{
290382
vtil::logger::log("\n\n>> %s \n", __FUNCTION__);

0 commit comments

Comments
 (0)