Skip to content

Commit 9260a1a

Browse files
committed
added stacktrace, better debugs, and hanging detection mechanism
1 parent 0dd49f1 commit 9260a1a

3 files changed

Lines changed: 158 additions & 37 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ target_link_options(${TARGET} PRIVATE
133133
$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Darwin>>:-fsanitize=address>
134134
)
135135

136+
# DbgHelp for stack traces on timeout (Windows only)
137+
target_link_libraries(${TARGET} PRIVATE $<$<PLATFORM_ID:Windows>:DbgHelp>)
138+
136139
# detect x86
137140
set(IS_X86 FALSE)
138141
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|i[3-6]86")

auxiliary/test_cli.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,17 @@ Write-Host "--json"
214214
$tmpJson = [System.IO.Path]::GetTempFileName() + ".json"
215215
try {
216216
$out, $code = invoke_bin @("--json", "--output", $tmpJson) $true 30000
217+
if ($out -and $out.Trim() -ne "") {
218+
Write-Host " [binary output]"
219+
$out -split "`n" | ForEach-Object { Write-Host " $_" }
220+
}
217221
if ($code -eq -99) {
218222
Fail-Test "--json timed out"
219223
Fail-Test "--json output missing expected keys"
224+
if (Test-Path $tmpJson) {
225+
$partial = Get-Content $tmpJson -Raw -ErrorAction SilentlyContinue
226+
if ($partial) { Write-Host " [partial json]`n$partial" }
227+
}
220228
} else {
221229
if ((Test-Path $tmpJson) -and (Get-Item $tmpJson).Length -gt 0) {
222230
ok "--json creates a non-empty output file"
@@ -228,6 +236,7 @@ try {
228236
ok "--json output contains expected keys"
229237
} else {
230238
Fail-Test "--json output missing expected keys"
239+
Write-Host " [json content] $jsonContent"
231240
}
232241
}
233242
} finally {

src/cli/output.cpp

Lines changed: 146 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,82 @@
1414
#include <unistd.h>
1515
#endif
1616

17+
#if (CLI_WINDOWS)
18+
#include <dbghelp.h>
19+
20+
static void print_thread_stacktrace(HANDLE hThread, const std::string& label) {
21+
// Freeze the thread so GetThreadContext returns a consistent snapshot.
22+
SuspendThread(hThread);
23+
24+
CONTEXT ctx = {};
25+
ctx.ContextFlags = CONTEXT_FULL;
26+
if (!GetThreadContext(hThread, &ctx)) {
27+
std::cerr << "[STACKTRACE] GetThreadContext failed (err " << GetLastError() << ")\n" << std::flush;
28+
return; // caller will TerminateThread; no Resume needed
29+
}
30+
31+
HANDLE hProc = GetCurrentProcess();
32+
33+
// SymInitialize must be called once per process.
34+
static bool sym_ready = false;
35+
if (!sym_ready) {
36+
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
37+
sym_ready = SymInitialize(hProc, nullptr, TRUE) != FALSE;
38+
}
39+
40+
STACKFRAME64 sf = {};
41+
DWORD machine;
42+
43+
#if defined(_M_X64)
44+
machine = IMAGE_FILE_MACHINE_AMD64;
45+
sf.AddrPC.Offset = ctx.Rip; sf.AddrPC.Mode = AddrModeFlat;
46+
sf.AddrFrame.Offset = ctx.Rbp; sf.AddrFrame.Mode = AddrModeFlat;
47+
sf.AddrStack.Offset = ctx.Rsp; sf.AddrStack.Mode = AddrModeFlat;
48+
#elif defined(_M_IX86)
49+
machine = IMAGE_FILE_MACHINE_I386;
50+
sf.AddrPC.Offset = ctx.Eip; sf.AddrPC.Mode = AddrModeFlat;
51+
sf.AddrFrame.Offset = ctx.Ebp; sf.AddrFrame.Mode = AddrModeFlat;
52+
sf.AddrStack.Offset = ctx.Esp; sf.AddrStack.Mode = AddrModeFlat;
53+
#else
54+
std::cerr << "[STACKTRACE] unsupported architecture\n" << std::flush;
55+
return;
56+
#endif
57+
58+
std::cerr << "[STACKTRACE] hung in " << label << ":\n";
59+
60+
alignas(SYMBOL_INFO) char sym_buf[sizeof(SYMBOL_INFO) + MAX_SYM_NAME];
61+
SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(sym_buf);
62+
63+
IMAGEHLP_LINE64 line_info = {};
64+
line_info.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
65+
66+
for (int n = 0; n < 32; ++n) {
67+
if (!StackWalk64(machine, hProc, hThread, &sf, &ctx,
68+
nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) {
69+
break;
70+
}
71+
if (sf.AddrPC.Offset == 0) { break; }
72+
73+
std::cerr << " #" << std::dec << n
74+
<< " 0x" << std::hex << std::setw(16) << std::setfill('0') << sf.AddrPC.Offset;
75+
76+
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
77+
sym->MaxNameLen = MAX_SYM_NAME;
78+
DWORD64 sym_disp = 0;
79+
if (sym_ready && SymFromAddr(hProc, sf.AddrPC.Offset, &sym_disp, sym)) {
80+
std::cerr << " " << sym->Name << "+" << std::dec << sym_disp;
81+
DWORD line_disp = 0;
82+
if (SymGetLineFromAddr64(hProc, sf.AddrPC.Offset, &line_disp, &line_info)) {
83+
std::cerr << " (" << line_info.FileName << ":" << line_info.LineNumber << ")";
84+
}
85+
}
86+
std::cerr << "\n";
87+
}
88+
std::cerr << std::dec << std::setfill(' ') << std::flush;
89+
// Caller TerminateThread; intentionally not ResumeThread here.
90+
}
91+
#endif
92+
1793
const char* color(const u8 score, const bool is_hardened) {
1894
if (arg_bitset.test(NO_ANSI)) {
1995
return "";
@@ -218,6 +294,60 @@ const char* get_vm_description(const std::string& vm_brand) {
218294
return "";
219295
}
220296

297+
#if (CLI_WINDOWS)
298+
// Run a single technique in a worker thread and wait up to TECHNIQUE_TIMEOUT_MS.
299+
// Returns {result, timed_out}. On timeout the flag is pushed to
300+
// VM::disabled_techniques so the VM::vmaware constructor won't re-run it.
301+
static constexpr DWORD TECHNIQUE_TIMEOUT_MS = 3000;
302+
303+
struct win_run_result { bool result; bool timed_out; };
304+
305+
static win_run_result win_run_technique(const VM::enum_flags flag) {
306+
struct runner_t {
307+
VM::enum_flags flag;
308+
bool result = false;
309+
static DWORD WINAPI run(LPVOID lp) noexcept {
310+
auto* self = static_cast<runner_t*>(lp);
311+
self->result = VM::check(self->flag);
312+
return 0;
313+
}
314+
};
315+
316+
runner_t runner {
317+
flag,
318+
false
319+
};
320+
321+
HANDLE h = CreateThread(nullptr, 0, runner_t::run, &runner, 0, nullptr);
322+
if (!h) {
323+
return {
324+
VM::check(flag),
325+
false
326+
};
327+
}
328+
329+
if (WaitForSingleObject(h, TECHNIQUE_TIMEOUT_MS) == WAIT_TIMEOUT) {
330+
TerminateThread(h, 0);
331+
CloseHandle(h);
332+
VM::disabled_techniques.push_back(flag);
333+
std::cerr << "[TIMEOUT] VM::" << VM::flag_to_string(flag)
334+
<< " did not finish within " << (TECHNIQUE_TIMEOUT_MS / 1000)
335+
<< "s, skipping\n" << std::flush;
336+
337+
return {
338+
false,
339+
true
340+
};
341+
}
342+
343+
CloseHandle(h);
344+
return {
345+
runner.result,
346+
false
347+
};
348+
}
349+
#endif
350+
221351
static void checker(const VM::enum_flags flag, const char* message) {
222352
std::string enum_name;
223353

@@ -245,43 +375,9 @@ static void checker(const VM::enum_flags flag, const char* message) {
245375
auto start_time = std::chrono::high_resolution_clock::now();
246376

247377
#if (CLI_WINDOWS)
248-
// Some Windows API calls (e.g. SetupDiGetClassDevsW device enumeration) can
249-
// stall indefinitely on certain hypervisors. Run each technique in a worker
250-
// thread so we can abort it and continue if it takes too long.
251-
constexpr DWORD TECHNIQUE_TIMEOUT_MS = 3000;
252-
253-
struct technique_runner {
254-
VM::enum_flags flag;
255-
bool result = false;
256-
257-
static DWORD WINAPI run(LPVOID lp) noexcept {
258-
auto* self = static_cast<technique_runner*>(lp);
259-
self->result = VM::check(self->flag);
260-
return 0;
261-
}
262-
};
263-
264-
technique_runner runner{ flag, false };
265-
HANDLE hThread = CreateThread(nullptr, 0, technique_runner::run, &runner, 0, nullptr);
266-
267-
bool result = false;
268-
269-
if (hThread) {
270-
if (WaitForSingleObject(hThread, TECHNIQUE_TIMEOUT_MS) == WAIT_TIMEOUT) {
271-
TerminateThread(hThread, 0);
272-
CloseHandle(hThread);
273-
// Prevent VM::vmaware constructor from re-running this technique.
274-
VM::disabled_techniques.push_back(flag);
275-
std::cerr << "[TIMEOUT] \"" << message << "\" (VM::" << VM::flag_to_string(flag)
276-
<< ") did not finish within " << (TECHNIQUE_TIMEOUT_MS / 1000)
277-
<< "s, skipping\n" << std::flush;
278-
return;
279-
}
280-
CloseHandle(hThread);
281-
result = runner.result;
282-
} else {
283-
result = VM::check(flag);
284-
}
378+
const win_run_result wres = win_run_technique(flag);
379+
if (wres.timed_out) { return; }
380+
const bool result = wres.result;
285381
#else
286382
const bool result = VM::check(flag);
287383
#endif
@@ -360,6 +456,19 @@ bool parse_disable_token(const char* token) {
360456
}
361457

362458
void generate_json(const char* output) {
459+
#if (CLI_WINDOWS)
460+
// Warm the memo cache with per-technique timeouts before constructing
461+
// VM::vmaware, so that any technique that hangs (e.g. CPU_HEURISTIC on
462+
// Azure Hyper-V) is aborted and disabled rather than blocking indefinitely.
463+
for (u8 i = VM::technique_begin; i < static_cast<u8>(VM::technique_end); ++i) {
464+
const VM::enum_flags flag = static_cast<VM::enum_flags>(i);
465+
if (is_disabled(flag) || is_unsupported(flag)) {
466+
continue;
467+
}
468+
win_run_technique(flag);
469+
}
470+
#endif
471+
363472
const VM::vmaware vm(VM::MULTIPLE);
364473

365474
std::vector<std::string> json;

0 commit comments

Comments
 (0)