diff --git a/docs/architecture.md b/docs/architecture.md index 612bd4cde9..eeb3a89d65 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1036,6 +1036,14 @@ remain the same. Core callers opt in through `PipelineOptions::AnalysisCache` and must reset it before input or policy mutation; image pointers alone do not establish validity. +Instruction-flow sweeps keep one `InstructionFlowScratch` per decoding worker. +The workspace reuses only the temporary LowOp vector's storage: operations +are cleared and the x87 state reset before every instruction, including after +an unsupported lift. Returned references and transfer facts own their values. +Disassembly pages own a workspace for that call; parallel access-width, +reference and string-reference scans retire theirs with each worker. No TLS, +session cache, new semantic shortcut or cross-instruction state is involved. + Function selection also retains decoded image data-access widths. Their exact ordered function address/size inventory is checked after synchronization; changed extents rebuild the widths and discard source using the former names. diff --git a/lib/sdk/capi/InstructionFlow.cpp b/lib/sdk/capi/InstructionFlow.cpp index de884358c1..5281ea84ba 100644 --- a/lib/sdk/capi/InstructionFlow.cpp +++ b/lib/sdk/capi/InstructionFlow.cpp @@ -120,8 +120,16 @@ StackMove stackMove(const TargetRegInfo &Registers, llvm::ArrayRef Ops) { InstructionFlow summarizeInstructionFlow(const BinaryImage &Img, Decoder &Dec, const DecodedInsn &DI, const TargetRegInfo *StackRegisters) { + InstructionFlowScratch Scratch; + return Scratch.summarize(Img, Dec, DI, StackRegisters); +} + +InstructionFlow +InstructionFlowScratch::summarize(const BinaryImage &Img, Decoder &Dec, + const DecodedInsn &DI, + const TargetRegInfo *StackRegisters) { InstructionFlow Flow; - std::vector Ops; + Ops.clear(); // Each row is lifted independently of its neighbours. Dec.resetX86FpuState(); try { @@ -352,7 +360,8 @@ constexpr uint64_t MaxUnsizedInstructions = 65536; /// direct jumps reach from \p Entry, short of another function's entry in /// \p Entries, in address order. 32-bit ARM, whose jumps can change the /// instruction set, reads up to the first terminator instead. -std::vector unsizedBody(const BinaryImage &Img, Decoder &Dec, va_t Entry, +std::vector unsizedBody(const BinaryImage &Img, Decoder &Dec, + InstructionFlowScratch &Scratch, va_t Entry, llvm::ArrayRef Entries) { const auto OtherEntry = [&](va_t Address) { return Address != Entry && @@ -370,7 +379,7 @@ std::vector unsizedBody(const BinaryImage &Img, Decoder &Dec, va_t Entry, if (OtherEntry(DI.Addr) || !Body.insert(DI.Addr).second) return false; if (FollowsJumps) { - const InstructionFlow Flow = summarizeInstructionFlow(Img, Dec, DI); + const InstructionFlow Flow = Scratch.summarize(Img, Dec, DI); if ((Flow.Kind == "jump" || Flow.Kind == "cjump") && Flow.Target != InvalidVA && !OtherEntry(Flow.Target) && !Body.count(Flow.Target)) @@ -387,7 +396,8 @@ std::vector unsizedBody(const BinaryImage &Img, Decoder &Dec, va_t Entry, bool decodeExtents( const BinaryImage &Img, llvm::ArrayRef> Extents, llvm::ArrayRef Entries, - llvm::function_ref + llvm::function_ref Visit) { // The parts of a function its unwind records keep apart from its body, // past other functions' code, are its code too: MSVC moves cold paths @@ -402,21 +412,23 @@ bool decodeExtents( DecoderFailed = true; return; } + InstructionFlowScratch Scratch; for (size_t Index = Claim(); Index < Total; Index = Claim()) { const auto [Entry, Size] = Extents[Index]; if (Size > 0) { (void)decodeNativeRange( Img, Local, Entry, Size, Size, [&](const DecodedInsn &DI, const uint8_t *, int) { - Visit(Index, Local, DI); + Visit(Index, Local, DI, Scratch); return true; }); } else { - for (const va_t Address : unsizedBody(Img, Local, Entry, Entries)) + for (const va_t Address : + unsizedBody(Img, Local, Scratch, Entry, Entries)) (void)decodeNativeRange( Img, Local, Address, 0, 1, [&](const DecodedInsn &DI, const uint8_t *, int) { - Visit(Index, Local, DI); + Visit(Index, Local, DI, Scratch); return true; }); } @@ -426,7 +438,7 @@ bool decodeExtents( Img, Local, Range.Begin, Range.End - Range.Begin, Range.End - Range.Begin, [&](const DecodedInsn &DI, const uint8_t *, int) { - Visit(Index, Local, DI); + Visit(Index, Local, DI, Scratch); return true; }); } @@ -442,7 +454,8 @@ bool isDataAccess(const InstructionRef &Ref) { bool decodeFunctions( const BinaryImage &Img, const FunctionPage &Page, - llvm::function_ref + llvm::function_ref Visit) { std::vector> Extents; Extents.reserve(Page.Functions.size()); @@ -463,9 +476,10 @@ collectDataAccessWidths(const BinaryImage &Img, std::vector>> Slots( Page.Functions.size()); const bool Decoded = decodeFunctions( - Img, Page, [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { - for (const InstructionRef &Ref : - summarizeInstructionFlow(Img, Dec, DI).Refs) + Img, Page, + [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { + for (const InstructionRef &Ref : Scratch.summarize(Img, Dec, DI).Refs) if (isDataAccess(Ref)) Slots[Index].emplace_back(Ref.To, Ref.Bytes); }); @@ -513,9 +527,10 @@ DataAccessScan::update(const BinaryImage &Img, std::vector>> Slots(Read.size()); if (!Read.empty() && !decodeExtents(Img, Read, NextEntries, - [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { + [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { for (const InstructionRef &Ref : - summarizeInstructionFlow(Img, Dec, DI).Refs) + Scratch.summarize(Img, Dec, DI).Refs) if (isDataAccess(Ref)) Slots[Index].emplace_back(Ref.To, Ref.Bytes); })) { diff --git a/lib/sdk/capi/InstructionFlow.h b/lib/sdk/capi/InstructionFlow.h index 772a66c8c5..ec773539e0 100644 --- a/lib/sdk/capi/InstructionFlow.h +++ b/lib/sdk/capi/InstructionFlow.h @@ -124,6 +124,19 @@ summarizeInstructionFlow(const BinaryImage &Img, Decoder &Dec, const DecodedInsn &DI, const TargetRegInfo *StackRegisters = nullptr); +/// One decoding worker's temporary lift storage. Only capacity survives a +/// call: every instruction starts with empty operations and fresh FPU state. +/// Results own their facts and never borrow this workspace. +class InstructionFlowScratch { +public: + InstructionFlow summarize(const BinaryImage &Img, Decoder &Dec, + const DecodedInsn &DI, + const TargetRegInfo *StackRegisters = nullptr); + +private: + std::vector Ops; +}; + /// The pointer the loader stored in a relocated slot, as a code address when /// the slot points to code. std::optional relocatedPointer(const BinaryImage &Img, va_t Slot); @@ -144,18 +157,19 @@ FunctionPage functionPage(llvm::ArrayRef Functions, va_t FirstEntry, FunctionPage functionPage(const Session &S, va_t FirstEntry, int MaxFunctions); /// Decodes each function of \p Page in \p Img with a decoder of its own, -/// functions in parallel, and calls \p Visit(Index, Decoder, Instruction) for -/// each of its instructions in address order: up to the function's recorded -/// size, or, when it has none, the instructions its own direct jumps reach -/// from its entry short of another function's entry (Page.Entries; on 32-bit -/// ARM, up to its first terminator), then the parts of it that lie apart -/// from its body (ExceptionInfo::outlyingChunks). One function's visits run -/// on one thread, so writing only to its own output keeps the result -/// independent of the thread count. False when a decoder could not be set -/// up. +/// functions in parallel, and calls Visit(Index, Decoder, Instruction, Scratch) +/// with one exclusive scratch workspace per decoding worker for each of its +/// instructions in address order: up to the function's recorded size, or, +/// when it has none, the instructions its own direct jumps reach from its +/// entry short of another function's entry (Page.Entries; on 32-bit ARM, up +/// to its first terminator), then the parts of it that lie apart from its +/// body (ExceptionInfo::outlyingChunks). One function's visits run on one +/// thread, so writing only to its own output keeps the result independent of +/// the thread count. False when a decoder could not be set up. bool decodeFunctions( const BinaryImage &Img, const FunctionPage &Page, - llvm::function_ref + llvm::function_ref Visit); /// The width of the reads and writes the instructions of each of \p diff --git a/lib/sdk/capi/NeverDCAPICallPaths.cpp b/lib/sdk/capi/NeverDCAPICallPaths.cpp index f76938477e..86378ba1f1 100644 --- a/lib/sdk/capi/NeverDCAPICallPaths.cpp +++ b/lib/sdk/capi/NeverDCAPICallPaths.cpp @@ -201,13 +201,14 @@ std::shared_ptr buildGraph(Session &S) { }; std::vector> Slots(Bodies.size()); // The references of one instruction of body \p Index. - const auto Collect = [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { + const auto Collect = [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { auto &Out = Slots[Index]; const auto Add = [&](std::optional To, uint8_t Kind) { if (To) Out.push_back({*To, Kind, DI.Addr}); }; - const InstructionFlow Flow = summarizeInstructionFlow(Img, Dec, DI); + const InstructionFlow Flow = Scratch.summarize(Img, Dec, DI); const bool Indirect = Flow.Kind == "icall" || Flow.Kind == "ijump"; if (Flow.Target != InvalidVA && !Flow.Kind.empty() && Flow.Kind != UnliftedFlow) { @@ -251,10 +252,11 @@ std::shared_ptr buildGraph(Session &S) { Bodies.size(), First + FunctionPage::MaxFunctionsPerPage); Page.Functions.assign(Bodies.begin() + First, Bodies.begin() + Last); Page.Entries = Entries; - if (!decodeFunctions( - Img, Page, [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { - Collect(First + Index, Dec, DI); - })) { + if (!decodeFunctions(Img, Page, + [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { + Collect(First + Index, Dec, DI, Scratch); + })) { S.setError("failed to initialize a decoder for the image"); return nullptr; } diff --git a/lib/sdk/capi/NeverDCAPIDisasm.cpp b/lib/sdk/capi/NeverDCAPIDisasm.cpp index 869e73d703..3f24e7a400 100644 --- a/lib/sdk/capi/NeverDCAPIDisasm.cpp +++ b/lib/sdk/capi/NeverDCAPIDisasm.cpp @@ -226,6 +226,7 @@ const char *neverd_disasm_json_ex(neverd_session_t Sess, neverd_va_t Addr, MaxInsns > 0 ? static_cast(MaxInsns) : (Span > 0 ? Span : 256); const TargetRegInfo &Registers = getTargetRegInfo(S->Img.Arch); + InstructionFlowScratch Scratch; const bool ModeKnown = decodeNativeRange( S->Img, S->Dec, Addr, Span, Limit, [&](const DecodedInsn &DI, const uint8_t *Bytes, int Sz) { @@ -255,7 +256,7 @@ const char *neverd_disasm_json_ex(neverd_session_t Sess, neverd_va_t Addr, Obj["operand_bits"] = std::move(Bits); } if (Options & (NEVERD_DISASM_FLOW | NEVERD_DISASM_STACK)) { - const InstructionFlow Flow = summarizeInstructionFlow( + const InstructionFlow Flow = Scratch.summarize( S->Img, S->Dec, DI, Options & NEVERD_DISASM_STACK ? &Registers : nullptr); if (Options & NEVERD_DISASM_FLOW) { @@ -328,9 +329,11 @@ const char *neverd_code_refs_json(neverd_session_t Sess, neverd_va_t FirstEntry, // Each function's references, merged in entry order. std::vector> Slots(Page.Functions.size()); const bool Decoded = decodeFunctions( - S->Img, Page, [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { + S->Img, Page, + [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { auto &Out = Slots[Index]; - const InstructionFlow Flow = summarizeInstructionFlow(S->Img, Dec, DI); + const InstructionFlow Flow = Scratch.summarize(S->Img, Dec, DI); if (Flow.Target != InvalidVA && !Flow.Kind.empty() && Flow.Kind != UnliftedFlow) Out.push_back({DI.Addr, Flow.Target, Flow.Kind}); diff --git a/lib/sdk/capi/NeverDCAPIStringRefs.cpp b/lib/sdk/capi/NeverDCAPIStringRefs.cpp index cbde5cf38d..bd5f013e6c 100644 --- a/lib/sdk/capi/NeverDCAPIStringRefs.cpp +++ b/lib/sdk/capi/NeverDCAPIStringRefs.cpp @@ -89,8 +89,10 @@ const char *neverd_string_refs_json(neverd_session_t Sess, std::vector> Slots(Page.Functions.size()); const BinaryImage &Img = S->Img; const bool Decoded = decodeFunctions( - Img, Page, [&](size_t Index, Decoder &Dec, const DecodedInsn &DI) { - const InstructionFlow Flow = summarizeInstructionFlow(Img, Dec, DI); + Img, Page, + [&](size_t Index, Decoder &Dec, const DecodedInsn &DI, + InstructionFlowScratch &Scratch) { + const InstructionFlow Flow = Scratch.summarize(Img, Dec, DI); // The instruction as neverd_disasm_json() spells it. const auto instruction = [&] { std::string Text = DI.Raw ? DI.Raw->mnemonic : ""; diff --git a/tools/neverd-worker/CMakeLists.txt b/tools/neverd-worker/CMakeLists.txt index 791ccd0511..910b7e84ad 100644 --- a/tools/neverd-worker/CMakeLists.txt +++ b/tools/neverd-worker/CMakeLists.txt @@ -244,6 +244,10 @@ if(BUILD_TESTING OR NEVERD_BUILD_TESTS) COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/data_presentation_cli_test.py" "$" "${_neverd_worker_test_cli}" --engine "$") set_tests_properties(NeverDWorkerCLIDataPresentation PROPERTIES TIMEOUT 180) + add_test(NAME NeverDWorkerCLIInstructionFlow + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/instruction_flow_cli_test.py" + "$" "${_neverd_worker_test_cli}") + set_tests_properties(NeverDWorkerCLIInstructionFlow PROPERTIES TIMEOUT 120) add_test(NAME NeverDWorkerCLIStackFrame COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/stack_frame_cli_test.py" "$" "${_neverd_worker_test_cli}" --cc "${CMAKE_C_COMPILER}") diff --git a/tools/neverd-worker/tests/instruction_flow_cli_test.py b/tools/neverd-worker/tests/instruction_flow_cli_test.py new file mode 100644 index 0000000000..e081210e34 --- /dev/null +++ b/tools/neverd-worker/tests/instruction_flow_cli_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""CLI and worker reference pages agree on synthetic instruction sweeps.""" +import argparse +import json +from pathlib import Path +import struct +import subprocess +import tempfile + +from transport_test import Client + +ENTRY = 0x400078 + + +def fixture(count): + # Explicit symbol extents avoid depending on a detector heuristic. Both + # 8-byte and 4-byte loads are followed by NOP/RET: scratch reuse must not + # give those trailing instructions a preceding instruction's reference. + target = ENTRY + 16 * count + code = bytearray() + expected = [] + names = bytearray(b"\0") + symbols = bytearray(24) + for index in range(count): + start = ENTRY + 16 * index + load = bytes.fromhex("488b05" if index % 2 == 0 else "8b05") + body = load + struct.pack("( + DataELFData - (DataELFEntry + Offset + Bytes.size() + 4)); + for (unsigned I = 0; I < 4; ++I) + Bytes += static_cast(Delta >> (8 * I)); + Bytes += std::string("\x90\xc3", 2); + Code.replace(Offset, Bytes.size(), Bytes); + } + const auto Input = write("scratch-accesses.elf", + makeDataELF(std::string("hello world\0", 12), Code)); + ASSERT_EQ(neverd_session_load(Session, Input.c_str()), 1); + auto &State = *neverd::sdk::toSession(Session); + State.Img.Symbols = {neverd::Symbol::makeFunc(DataELFEntry, 9), + neverd::Symbol::makeFunc(DataELFEntry + 0x20, 8)}; + neverd::sdk::SessionTestAccess::resetFunctions(State); + std::string Previous; + for (unsigned Repeat = 0; Repeat < 3; ++Repeat) { + const auto Text = + takeString(neverd_code_refs_json(Session, DataELFEntry, 2)); + auto Parsed = llvm::json::parse(Text); + ASSERT_TRUE(static_cast(Parsed)) << Text; + const auto *Rows = Parsed->getAsObject()->getArray("refs"); + ASSERT_TRUE(Rows && Rows->size() == 2) << Text; + for (unsigned I = 0; I < 2; ++I) { + const auto *Row = (*Rows)[I].getAsArray(); + ASSERT_TRUE(Row && Row->size() == 4) << Text; + EXPECT_EQ(std::stoull((*Row)[0].getAsString()->str(), nullptr, 16), + DataELFEntry + (I ? 0x20 : 0)); + EXPECT_EQ(std::stoull((*Row)[1].getAsString()->str(), nullptr, 16), + DataELFData); + EXPECT_EQ((*Row)[2].getAsString().value_or(""), I ? "write" : "read"); + EXPECT_EQ((*Row)[3].getAsInteger().value_or(0), I ? 4 : 8); + } + if (Repeat) + EXPECT_EQ(Text, Previous); + Previous = Text; + const auto Strings = + takeString(neverd_string_refs_json(Session, nullptr, DataELFEntry, 2)); + auto StringPage = llvm::json::parse(Strings); + ASSERT_TRUE(static_cast(StringPage)) << Strings; + const auto *StringRows = StringPage->getAsObject()->getArray("refs"); + ASSERT_TRUE(StringRows && StringRows->size() == 2) << Strings; + const auto *Widths = + neverd::sdk::SessionTestAccess::dataAccessWidths(State); + ASSERT_TRUE(Widths); + EXPECT_EQ(Widths->at(DataELFData), 0u); + } +} + TEST_F(SessionCAPITest, WarmingScansTheDataAccessWidthsTheFirstViewReads) { const auto Input = write("widths.elf", accessWidthCacheELF()); std::string Cold;