Repository navigation
fix(analysis): C++ anonymous namespaces stop aborting the load, and symbolnamerepair backstops the rest (DIV-88) - #335
Merged
Conversation
…ymbolnamerepair backstops the rest (DIV-88) kuna could not load an unstripped C++ binary that uses an anonymous namespace -- at all, by any command. `Database::find_create_scope_from_symbol_name` nests one Scope per `::` component and `attach_scope` rejects an EMPTY one. The symbol table is installed inside `load file`, so that `LowlevelError` escapes `bootstrap_from_object` rather than costing one symbol: `decompile-all`, `functions` and `decompile-project` all answered `could not build an architecture for <binary>: Non-global scope has empty name` and emitted nothing. The empty component came from the name-only reduction. Itanium renders `_GLOBAL__N_...` as `(anonymous namespace)`, `strip_bracket_groups` deletes every balanced bracket group, and `leveldb::(anonymous namespace)::HandleDumpCommand` therefore reduced to `leveldb::::HandleDumpCommand`. An anonymous namespace is the ordinary way C++ gives a definition internal linkage, so this was most unstripped C++, not an exotic input. Diagnosed during the #270 leveldb work and recorded there as out of scope; this closes it. Two independent parts: 1. No flag (a strict fix). `name_anonymous_namespaces` rewrites both spellings -- Itanium `(anonymous namespace)` and MSVC `` `anonymous namespace' `` -- to the identifier `anonymous_namespace` BEFORE the bracket strip, matching what `sanitize_class_name` already gives the same construct. The identifier spelling is forced, not cosmetic: `load function` splits its argument on whitespace, so a name with a space is unaddressable from the CLI, the console and every stage XML; and a scope name reaches emitted C unsanitized, where `(` opens a parameter list. `demangle_raw` keeps the faithful c++filt text, unrewritten. 2. `symbolnamerepair` (on|off, default on). A name can be degenerate with no demangler involved, and symbol-name bytes are attacker-controlled data that no header check validates -- a hostile binary buys a total denial of analysis for a few `.strtab` bytes, which matters given the reporting binary was malware. Only the empty component is treated as degenerate. Loader-tier, so the gate is the `KUNA_SYMBOLNAMEREPAIR` env bridge (the `relocrebase` pattern), exported by both CLI surfaces; off restores the hard error, which is what someone investigating a binary's symbol table itself wants. Collateral on the Itanium/ELF world is zero and structurally so: on a binary with no `::`-qualified and no mangled symbol neither path executes. `decompile-all --json` is byte-identical on 8 real binaries, four of them C++. The one deliberate output change on a binary that already loaded is the MSVC arm (`` `anonymous namespace'::Bar::foo `` -> `anonymous_namespace::Bar::foo`); the old spelling is not valid C, so it only ever corrects wrong output. Witnesses that go from zero output to full output: the reporting `native_payload.dll` (MinGW PE32+, 1184 `_GLOBAL__N_` symbols) now exports 4061 of 4064 functions in 92.3 s; `libleveldb.so.1.23.0` 3751 functions / 273 `anonymous_namespace::` names; `leveldbutil` 58 / 7. Speed: interleaved min-of-9 `decompile-all --json` on grep, +0.75%, inside the box's measured noise floor (a null A/B of the same binary against itself spreads 0.91 s over 9 reps). Tests: `tests/stages/kuna-symbolnamerepair.xml` (6 assertions over the new `anon_namespace_x86_64` fixture, pinning all three anonymous-namespace shapes, the call-site rendering, and a min=0 max=0 guard that no `::::` survives) plus `kuna-console/tests/verify_symbolnamerepair.rs` (two-pass: the anonymous-namespace fixture loads with the backstop forced OFF, proving the two parts are independent; `hostile_scope_x86_64` -- a 4.7 KB ELF whose `.symtab` names a function `a::::b`, built reproducibly from checked-in assembly -- loads on and restores the exact error off), four demangle unit tests and the gate's own env-token test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FPeR8e2Bhv3Hm8gux8RXb
This was referenced Aug 27, 2026
Closed
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.
kuna decompile-project ./native_payload.dll(a MinGW-built malware DLL) failed witherror: could not build an architecture ...: Non-global scope has empty name. The binary didnot load at all, by any command. The bug turned out not to be malware-specific: kuna could
not load any binary using a C++ anonymous namespace, which is most unstripped C++.
Root cause
Database::find_create_scope_from_symbol_namenests one Scope per::component andattach_scoperejects an EMPTY one. The symbol table is installed insideload file, so thatLowlevelErrorescapesbootstrap_from_objectrather than costing one symbol:decompile-all,functionsanddecompile-projectall answeredcould not build an architectureand emittednothing. (
kuna decompilealso produced nothing but printed the generic(unsupported/!recognized binary), becausecheck_errorsmaps the subprocess'sCould not create architectureonto that string — which is what made this undiagnosable fromthat one surface. Separate driver-tier defect; not fixed here.)
The empty component came from the name-only reduction: Itanium renders
_GLOBAL__N_...as(anonymous namespace),strip_bracket_groupsdeletes every balanced bracket group, andleveldb::(anonymous namespace)::HandleDumpCommandtherefore reduced toleveldb::::HandleDumpCommand.Diagnosed during the #270 leveldb work and recorded there as out of scope; this closes it.
Two independent parts
1. The root cause — no flag (a strict fix; the wrong output was no output).
name_anonymous_namespacesrewrites both spellings — Itanium(anonymous namespace)and MSVC`anonymous namespace'— to the identifieranonymous_namespaceBEFORE the bracket strip,matching what
kuna_itaniumrtti.rs::sanitize_class_namealready gives the same construct.The identifier spelling is forced, not cosmetic:
load functionsplits its argument onwhitespace, so a name with a space is unaddressable from the CLI, the console and every stage
XML; and a scope name reaches emitted C unsanitized, where
(opens a parameter list.demangle_rawkeeps the faithful c++filt text, unrewritten.2.
symbolnamerepair(on|off, default on) — the backstop. A name can be degenerate with nodemangler involved, and symbol-name bytes are attacker-controlled data that no header check
validates: a hostile binary buys a total denial of analysis for a few
.strtabbytes. Only theempty component is treated as degenerate. Loader-tier, so the gate is the
KUNA_SYMBOLNAMEREPAIRenv bridge (the
relocrebasepattern), exported by both CLI surfaces. Off restores the harderror, which is what someone investigating a binary's symbol table itself wants.
Evidence
Collateral on the Itanium/ELF world is zero and structurally so: on a binary with no
::-qualified and no mangled symbol neither path executes.decompile-all --jsonisbyte-identical on 8 real binaries, four of them C++ (faillog, grep, sort, libselinux.so.1,
cpp_mangled_x86_64, cpp_noreturn_x86_64, cppsig_x86_64.so, cxxthrow_noreturn_x86_64).
The one deliberate output change on a binary that already loaded is the MSVC arm:
`anonymous namespace'::Bar::foo→anonymous_namespace::Bar::foo. Verified on apurpose-built fixture, since no tracked
.exe/.obj/.dllin the repo contains a?A0xsymbol. Unflagged deliberately: the old spelling is not valid C and its embedded space makes the
name unaddressable through
load function.Witnesses that go from zero output to full output:
native_payload.dll(MinGW PE32+, 2.7 MB, 1184_GLOBAL__N_symbols).c/.h/.asmexportlibleveldb.so.1.23.0anonymous_namespace::namesleveldbutilSpeed: interleaved min-of-9
decompile-all --jsonon grep, base 13.649 s vs branch13.751 s (+0.75%) — inside the box's measured noise floor (a null A/B of the same binary against
itself spreads 0.91 s over 9 reps), and necessarily so, since grep is C and reaches neither path.
Gates:
make test675/675 PARITY OK ·make test-stages538/538 PARITY OK ·make rust-testgreen ·make check-specgreen in both lenient and strict mode.Catalog 118 → 119 settables (analysis tier 42 → 43). Stages corpus 214 → 215 files.
Tests
tests/stages/kuna-symbolnamerepair.xml— 6 assertions over the newanon_namespace_x86_64fixture: all three anonymous-namespace shapes, the call-site rendering, and a
min=0 max=0guard that no
::::survives. Default arm only, and that is a real limit rather than anoversight: the broken arm is a LOAD failure and
optionrequires a live image, so the gatethat restores it has to be set before
load filethrough its env bridge, which no scriptcommand can do. A regression is still loud — a failed
load fileerrors the script out andthe baseline comparison reports REGRESSED.
kuna-console/tests/verify_symbolnamerepair.rs— two-pass. The anonymous-namespace fixtureloads with the backstop forced OFF, proving the two parts are independent rather than one
masking the other;
hostile_scope_x86_64(a 4.7 KB ELF whose.symtabnames a functiona::::b, built reproducibly from checked-in assembly, no byte-patching) loads on and restoresthe exact
Non-global scope has empty nameerror off.kuna-analysisdemangle unit tests and the gate's own env-token test.Follow-ups found while investigating (not fixed here)
kuna decompilemasks this whole class of failure viacheck_errors(above).amplifies ~1.46 KB of RSS per
::(a 40 MB.strtab⇒ ~19 GB ⇒ OOM); anst_sizewhose low32 bits are zero fails the analysis commit; a newline or
*/in a symbol name lands verbatimin emitted C.
🤖 Generated with Claude Code
https://claude.ai/code/session_011FPeR8e2Bhv3Hm8gux8RXb