feat(labels): recover C++ symbols and normalize language metadata - #210
Closed
r0ny123 wants to merge 8 commits into
Closed
feat(labels): recover C++ symbols and normalize language metadata#210r0ny123 wants to merge 8 commits into
r0ny123 wants to merge 8 commits into
Conversation
DelphiPythiaProvider was only used by LanguageAnalyzer to seed candidate addresses; its recovered VMT/method-table names were discarded because it was never registered in RecursiveDisassembler._addLabelProviders. Register it as a symbol provider (mirroring DelphiReSymProvider, which is used in both places) so resolveSymbol applies its names to functions. Validation: make lint, pytest tests/testDelphiPythiaProvider.py
The unrecognized version/bitness ValueError messages interpolated the raw integer after a '0x' prefix, rendering the marker in decimal (e.g. '0x4294967281'). Use hex format specifiers so the diagnostic matches the actual pclntab magic. No behavior change; the error is already caught by the caller's reraise_non_operational_exception path.
Resolve far more import-by-ordinal references to real API names, improving readability and cross-sample fingerprint stability. Only explicitly-assigned (frozen) export ordinals are added, sourced from the ReactOS .spec files that mirror the Windows ordinal ABI: - ws2_32.dll: Winsock classic set (1-23, 51-57, 101-116, 151) - wsock32.dll: full frozen legacy set incl. MSWSOCK forwarders (AcceptEx, TransmitFile, GetAcceptExSockaddrs) - oleaut32.dll: complete ordinal-only export table (Var*/SafeArray*/TypeLib) Drops the previously-incorrect ws2_32 97/98/99 entries (getaddrinfo/ freeaddrinfo/getnameinfo are by-name exports, not stable ordinals). System DLLs (kernel32/user32/advapi32/...) are intentionally excluded: they export by name with non-stable ordinals. Validation: make lint, pytest tests/testDefinitionsExpansion.py
Address PR review: - Move _updateLabelProviders() to after bitness is finalized so bitness-keyed providers (DelphiPythiaProvider) parse correctly for raw buffers whose bitness is only known after probeBitness() (P2). - Revert the expanded OrdinalHelper comment block to its original two lines, per the repo no-comments convention (P1). Validation: make lint, make test (668 passed, 1 skipped)
Recover and demangle all defined ELF exports and relocation imports, keep data exports separate from function candidates, and make C++/Rust language evidence symbol-aware. Normalize report language metadata to score-only maps, preserve legacy report loading, switch to pycxxfilt, and raise the supported Python floor to 3.11. Pattern: PAT-SMDA-030 Validation: make lint, make test (692 passed, 1 skipped), package build BREAKING CHANGE: metadata.language no longer serializes private guess/count keys or backend string values, and SMDA now requires Python 3.11+.
Guard VB import-library normalization against malformed LIEF names and select the active thin slice before scanning C++ symbol evidence in fat Mach-O binaries. Document pycxxfilt double-underscore Mach-O support with a direct regression test. Patterns: PAT-SMDA-003, PAT-SMDA-030 Validation: make lint, make test (695 passed, 1 skipped)
The functional Mach-O corpus can exceed a 20-second wall-clock budget on contended Python 3.14 runners even though the same commit passes on another runner. Align its guard with the existing 120-second AArch64 corpus convention without changing production timeout behavior. Validation: Ruff check/format, focused corpus suite, full suite (695 passed, 1 skipped)
Address review findings on the C++ symbol recovery change. ELF relocation imports stay undecorated at the source, so API references keep matching PE and Mach-O import names; demangling moves up into ElfSymbolProvider.parseImports, which only feeds the label view. Exported symbol recovery now rejects entries whose value is not a virtual address: thread-local symbols carry a TLS-block offset, and SHN_COMMON / SHN_ABS are not laid-out definitions. Address aliases keep the first name the dynamic symbol table spells so repeated runs agree. The defined-symbol predicate is shared with Rust ELF symbol recovery, which had the same undefined-dynsym exposure. The language score map stays score-only. Picking a single language from it is not a plain maximum, so the README documents the rule instead: go and rust win outright above 0.5, because a build ID, pclntab header, or demangled Rust symbol is conclusive while the other scores are graded. Also: stop the C++ symbol scan once the score saturates, recognize MSVC decorated data names, accept big-endian Go pclntab headers, and make the Mach-O corpus per-sample timeout a 45s default tunable by environment instead of a blanket 120s.
danielplohmann
approved these changes
Jul 28, 2026
Owner
danielplohmann
added a commit
that referenced
this pull request
Jul 28, 2026
…ry-tier2-rebased feat(labels): recover C++ symbols and normalize language metadata (rebased #210)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This completes the next label-recovery tier for C++ binaries and makes serialized language metadata consistent: every defined ELF dynamic export is recovered (including data objects such as vtables and RTTI), Itanium and MSVC C++ names are demangled in process, language identification is driven by validated symbol evidence instead of prefix guesses, and
metadata.languagebecomes a score-only map. Released as 4.4.0 with Python 3.11+ support.It builds on #209 (tier-1). Until that merges, this PR's commit range includes those four commits; the new work is
2fcf8bb..cf95489.Motivation
ELF exports were treated as function-only metadata. On a representative ELF sample that exposed 3 function exports even though the dynamic symbol table holds 26 defined exports: 3 functions and 23 objects, the objects being useful C++ metadata (vtables, typeinfo, typeinfo names).
Three related problems sat on the same path:
c++filt/llvm-cxxfiltsubprocesses) did not consistently cover ELF relocation imports and Mach-O symbols, so results varied per machine;_ZN-prefix checks classified ordinary Itanium C++ symbols as Rust;metadata.languagemixed public scores with internal counters and a private guess, and used backend-specific string values.Changed behavior
ELF export and import recovery
xmetadata.exported_symbolsis new: every defined ELF dynamic export, including data symbols, keyed by virtual address. Thread-local symbols are excluded because their value is an offset inside the TLS block rather than an address, andSHN_COMMON/SHN_ABSentries are excluded because they are not laid out in the image. Aliases sharing one address (for example the C1/C2 constructor pair) keep the first name the dynamic symbol table spells, so repeated runs agree.xmetadata.exported_functionsand the legacyxmetadata.symbolsmap stay function-only, so data objects never become function-entry candidates.xmetadata.imported_functionsis demangled. API references (SmdaFunction.apirefs) deliberately keep the undecorated import name, so they stay comparable with PE and Mach-O imports and with previously produced reports.On the representative ELF sample: 26 / 26 defined exports recovered, split 3 functions and 23 objects; 241 relocation-backed import slots recovered; no Itanium-mangled labels left in the report label maps.
C++ and Rust evidence
pycxxfilt's vendored LLVM demangler replaces the host-tool subprocesses, so demangling is deterministic across machines and available on Windows.Language report contract
metadata.languagecontains onlylanguage -> float scoreentries. Internal counters and the private guess are gone.goandrustwin outright above 0.5, since a build ID, pclntab header, or demangled Rust symbol is conclusive while the other scores are graded evidence. The README documents this.{".net": 1.0}and Dalvik reports use{"dalvik": 1.0}instead of backend-specific strings.SmdaReport.fromDictnormalizes legacy string values and removes private legacy keys, so older reports load into the new contract.Deliberately unchanged
The Delphi VMT scan stays gated on the
TObjectmarker rather than on a valid PE header, and the Visual Basic runtime lookup is likewise ungated. Memory dumps that start below the MZ header are a primary input for SMDA, and a header gate would silently disable recovery for exactly those samples. The scan result is memoized and shared with Delphi candidate recovery, so a real Delphi image pays for it once either way.Compatibility
This is a breaking report and runtime change:
pycxxfilt>=0.1.0is a new runtime dependency and replaces host-tool demangling.metadata.languageas a score map and must not read_guess,_count_thiscalls,_count_delphi_objects, ordelphi_kb_file.xmetadata.exported_symbolsis additive; the existing function-oriented maps keep their meaning.Validation
make lint— cleanmake test— 701 passed, 1 skippedpython -m pip check— cleanThe large AArch64 Mach-O corpus test previously used a 20-second per-sample wall-clock guard that tripped twice on contended runners. Profiling showed the C++ symbol scorer costs roughly 1 ms per pass on the largest corpus sample, so it was not the cause and a sampled scorer was rejected as inaccurate. The guard is now a 45-second default, raisable through
SMDA_TEST_CORPUS_TIMEOUTon a slow runner. Production timeout behavior is unchanged.