From bc03500e35c612ea4c40060dbe84fd2a116c97b8 Mon Sep 17 00:00:00 2001 From: Letenz <58560745+Letenz@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:55:50 +0800 Subject: [PATCH] [sdk] Reuse temporary lift storage within instruction sweeps --- docs/architecture.md | 8 ++ lib/sdk/capi/InstructionFlow.cpp | 33 ++++-- lib/sdk/capi/InstructionFlow.h | 19 +++- lib/sdk/capi/NeverDCAPIDisasm.cpp | 9 +- lib/sdk/capi/NeverDCAPIStringRefs.cpp | 6 +- tools/neverd-worker/CMakeLists.txt | 4 + .../tests/instruction_flow_cli_test.py | 100 ++++++++++++++++++ unittests/SessionCAPITests.cpp | 58 ++++++++++ 8 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 tools/neverd-worker/tests/instruction_flow_cli_test.py diff --git a/docs/architecture.md b/docs/architecture.md index d5d9f5fb49..8e26d793f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1005,6 +1005,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 4e7a4f2e24..ddd0f91d0b 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 { @@ -323,7 +331,8 @@ namespace { /// recorded size. bool decodeExtents( const BinaryImage &Img, llvm::ArrayRef> Extents, - llvm::function_ref + llvm::function_ref Visit) { // A function without a recorded size is decoded only up to its first // terminator; this bounds a corrupt or missing extent. @@ -341,12 +350,13 @@ bool decodeExtents( DecoderFailed = true; return; } + InstructionFlowScratch Scratch; for (size_t Index = Claim(); Index < Total; Index = Claim()) { const auto [Entry, Size] = Extents[Index]; const uint64_t Limit = Size > 0 ? Size : MaxUnsizedInstructions; (void)decodeNativeRange(Img, Local, Entry, Size, Limit, [&](const DecodedInsn &DI, const uint8_t *, int) { - Visit(Index, Local, DI); + Visit(Index, Local, DI, Scratch); return Size > 0 || !Local.isFunctionTerminator(DI); }); @@ -356,7 +366,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; }); } @@ -372,7 +382,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()); @@ -392,9 +403,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); }); @@ -436,9 +448,10 @@ DataAccessScan::update(const BinaryImage &Img, std::vector>> Slots(Read.size()); if (!Read.empty() && !decodeExtents(Img, Read, - [&](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 3bcee224c5..8f7b7bbbc6 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); @@ -141,7 +154,8 @@ 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 +/// functions in parallel, and calls Visit(Index, Decoder, Instruction, Scratch) +/// with one exclusive scratch workspace per decoding worker for /// each of its instructions in order: up to the function's recorded size, or /// to its first terminator when it has none, then the parts of it that lie /// apart from its body (ExceptionInfo::outlyingChunks). One function's @@ -150,7 +164,8 @@ FunctionPage functionPage(const Session &S, va_t FirstEntry, int MaxFunctions); /// 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/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;