Repository navigation
[AUTOMATED] fix(p1): symbolnamechars — a symbol name's raw bytes stop restructuring the C document they are printed into (DIV-94) - #347
Merged
Conversation
mahaloz
force-pushed
the
feat/symbolnamechars
branch
from
August 27, 2026 20:20
b19926d to
26ba14c
Compare
…ing the C document they are printed into (DIV-94) [AUTOMATED] A symbol name reached emitted C verbatim, and nothing between `.strtab` and the printer validated one byte of it. The name is printed into the `// Function: <name> @ <addr>` header comment, the `.h` prototype, the definition, every call site and the `.asm` label, so three shapes restructured the C document rather than merely looking odd: a `*/` closed the header comment early and turned the rest of the line into code; a raw 0x0a split all five of those renderings across two lines each; and a `//` commented out the remainder of the line, parameter list and `;` included. A fourth broke identity rather than syntax: the name is decoded with `String::from_utf8_lossy`, so two symbols differing only in an invalid byte became the SAME `String` and the export carried two definitions and two prototypes with one name. Symbol-name bytes are attacker-controlled data no header check validates, so all four cost a few `.strtab` bytes. `symbolnamechars` (off|safe|ident, default safe) sanitizes at the MINT, not the printer: `kuna decompile <name>`, `load function` and the DB scope path all key on the string in `prog.symbols`, so a printer-side rewrite would put a name in the .c that cannot be handed back to the CLI. It runs after the demangler and before the scope splitter, at both mints -- the loader's .symtab/PLT/.dynsym/ET_REL and data-symbol walks, and the analysis passes' recovered names (DWARF, Go pclntab, PDB, RTTI). VALUED and not a bool, and that is the measured half: the most common name in the wild that is not valid C is gcc's clone suffix (err_fatal.constprop.0, main.part.1, add_fdes.cold), which `safe` is a measured no-op on and `ident` rewrites -- which is why `ident` must be reachable and must not be the default. Each rewritten byte becomes its `_x<hh>` hex escape rather than `_`, because `_` is not injective and would reproduce the redefinition defect with a different trigger. Two unflagged strict fixes ride along: printc's `/* renamed from "<raw>" */` note interpolated a RAW type name into a comment (the identical hole `sanitize_type_name` closes) and now escapes what it quotes; and `kuna_itaniumrtti (sanitize_class_name)` becomes a one-line wrapper over the hoisted shared `sanitize_ident_chain`, keeping its own unit tests as the regression net. Collateral is zero and measured twice: `decompile-all --json` is byte-identical on 17 real binaries (base-vs-branch AND off-vs-default), including two C++ ones and betaflight_STM32F405.elf; and `kuna functions` off-vs-default over every one of the 92 tracked ELF/PE/Mach-O files in the repo rewrites 0 names. Speed (interleaved min-of-4 decompile-all, box running several agents): grep -4.78%, libselinux +1.36%, regglobal_fmt -0.76%. Gates: make test 675/675 PARITY OK (no re-pin), make test-stages 547/547 PARITY OK, make rust-test green, make check-spec green lenient + strict, kuna catalog --check OK. Catalog 119 -> 120 (analysis tier 43 -> 44), stages corpus 215 -> 216, baseline-stages 538 -> 547 keys. Closes #340 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KpG7qK6BDFZyPnyo4r1c1
mahaloz
force-pushed
the
feat/symbolnamechars
branch
from
August 27, 2026 20:27
26ba14c to
1e42e54
Compare
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.
A symbol name reaches emitted C verbatim, and nothing between
.strtaband theprinter validates one byte of it.
The name is printed into the
// Function: <name> @ <addr>header comment, the.hprototype, the definition, every call site and the.asmlabel. Threeshapes therefore restructure the C document rather than merely looking odd:
*/closes the header comment early, so the@ 0x401020after it becomes code;0x0asplits all five of those renderings across two lines each(
void a\nb(void)declares nothing, andkuna functionsprints its TSV row ontwo lines);
//comments out the rest of the line, parameter list and;included.A fourth breaks identity rather than syntax: the name is decoded with
String::from_utf8_lossy, so two symbols differing only in an invalid byte(
a\x80bat one address,a\x81bat another) become the sameString— theexport carries two definitions and two prototypes with one name, and no reader
can tell which call went where.
Symbol-name bytes are attacker-controlled data no header check validates, so all
four cost a few
.strtabbytes.The option
symbolnamechars,off|safe|ident, defaultsafe, loader-tier(
KUNA_SYMBOLNAMECHARSenv bridge, thesymbolnamerepairpattern), exported byboth CLI surfaces.
offsafeident::componentIt is VALUED and not a bool, and that is the measured half. The most common
name in the wild that is not valid C is not hostile at all — it is gcc's clone
suffix (
err_fatal.constprop.0,main.part.1,add_fdes.cold,__x86.get_pc_thunk.bx), which appears on most-O2binaries and on sixfixtures checked into this repo.
safeis a measured no-op on every one ofthem (see Collateral);
identrewrites them all, which is exactly why it must bereachable and must not be the default.
The exact
safesetApplied to
&[u8], before any UTF-8 decode. A byte is rewritten iff it is0x00–0x1F,0x7F) — covers\n,\r,\t,\0;",'or\;*or/that forms*/,/*or//with its neighbor (bothcharacters of the pair are rewritten; a LONE
*or/is left alone — it isnot a comment delimiter, so rewriting it buys nothing structural and costs
collateral);
Everything else —
.$@:-+<>();{}and allvalid multi-byte UTF-8 — is untouched.
::survives both modes, because thescope splitter reads it.
identnever DELETES a name either: its reduction dropsempty components, so a name made only of non-identifier characters (
***) wouldreduce to nothing and put every such symbol under one empty name — the very
collapse this option exists to prevent. Such a name is escaped whole instead
(
_x2a_x2a_x2a), still an identifier and still injective.Two deliberate deviations from the shape this was scoped with, both argued in
the module header:
//is in the set. It was not in the original list, butvoid a//b(void)comments out the parameter list and the
;— the same structural corruptionthe other entries exist to stop. No clone suffix, mangled symbol or Rust path
contains
//, so it costs no collateral._x<hh>, not_. A single_is notinjective:
a"b,a'banda\nball collapse toa_b, which reproduces theexact redefinition defect the invalid-UTF-8 half exists to fix, merely with
a different trigger. One escape scheme for the whole set keeps the rewrite
injective, and costs nothing legible because
safefires on no real name.Mint time, not print time
kuna decompile <name>, the console'sload functionand the DB scope path allkey on the string in
prog.symbols. A printer-side rewrite would put a name inthe
.cthat cannot be handed back to the CLI — for an agent-first tool, a worsebug than the one being fixed. So the sanitizer runs where the name is minted:
after the demangler (it sees the reduced name, not the
_ZN…envelope, andidentdoes not fold the demangler's output twice) and before the scope splitter(so it never contends with
symbolnamerepairover the same empty component), atboth mints — the loader's
.symtab/PLT/.dynsym/ET_REL and data-symbol walks inkuna-analysis/src/loadimage_object.rs, and the analysis passes' recovered names(DWARF
DW_AT_name, Gopclntab, PDB, RTTI) viapass.rs (AnalysisOutput::sanitize_names). One string then servesprog.symbols,kuna functions,kuna decompile <name>, the.c, the.hand the.asm.Two unflagged strict fixes ride along
printc.rs's/* renamed from "<raw>" */note interpolated a raw type nameinto a comment — the identical injection hole in the construct
sanitize_type_namewas written to close. It now escapes what it quotes. Noflag: it only ever corrects wrong output.
kuna_itaniumrtti.rs (sanitize_class_name)becomes a one-line wrapper over thehoisted shared
sanitize_ident_chain, soidentand the RTTI class-namerecovery are one implementation. The RTTI path stays unconditional — the gate
decides only whether the loader's names go through it — and its six existing
unit tests are kept as the regression net for the hoist.
Witness —
decompile-projecton the new fixture, verbatimhostile_symname_x86_64: a 5 KB x86-64 ELF built reproducibly from checked-inassembly plus a checked-in
.strtabpatch script (GNU as will not put a raw0x0a,0x80or0x81inside a symbol name, so those three names are declaredas same-length ASCII placeholders and patched; the
*/,//andd*/xnamesassemble verbatim). Six symbols, one per half of the defect.
Before (
--option symbolnamechars off== today'smain):After (the shipped default):
Six distinct names, and the round-trip holds — every name
kuna functionsprintsresolves through
kuna decompile <name>:Collateral
decompile-all --jsonis byte-identical on 17 real binaries, measured twiceover — base-vs-branch (the whole PR,
printcincluded) andoff-vs-default (theoption alone):
Does
saferewrite ANY name on ANY real binary? No — and that is proved, notassumed.
kuna functionsoff-vs-default over every one of the 92 trackedELF/PE/Mach-O files in the repo rewrites 0 names:
The harness is not vacuous — pointed at the new hostile fixture (untracked at
scan time) it reports all five function names plus the data symbol as changed.
That covers the six clone-suffix fixtures the scout flagged
(
err_fatal.constprop.0,main.part.1,add_fdes.cold,.L10,__x86.get_pc_thunk.bx,0000001b.plt_call.printf@@GLIBC_2.17,.ctors.65535).identrewrites every one of them, e.g. onnoreturn_error_x86_64:Speed
Interleaved, alternating arms, min-of-4
decompile-all --json. The box wasrunning several other agents concurrently, which is why the spread goes both
directions and why min-of-N over interleaved runs is the only honest form here.
All inside the 5% budget, and the sign flips between binaries — this is noise,
and structurally it has to be:
safe's pre-scan returns a borrow and allocatesnothing for a clean name, and the gate is read once per symbol walk rather than
once per symbol.
Tests
tests/stages/kuna-symbolnamechars.xml— 9 assertions over the new fixture(the
*/name, the newline name, both invalid-UTF-8 names as distinctspellings, a call site, the data symbol, and two
min=0 max=0guards that nocomment delimiter survives anywhere). DEFAULT arm only, and that is a real
limit rather than an oversight:
symbolnamecharsis consumed insideload file, and a script'soptionrequires a live image, so no stage commandcan set the mode before the load it governs. Same limit, same reason, as
tests/stages/kuna-symbolnamerepair.xml(fix(analysis): C++ anonymous namespaces stop aborting the load, and symbolnamerepair backstops the rest (DIV-88) #335) — cited there too.decompiler/crates/kuna-console/tests/verify_symbolnamechars.rs— thethree-way coverage the XML cannot carry, 4 e2e tests:
offrestores every halfincluding the two-symbols-one-
Stringcollapse;safeneutralizes all four andkeeps six symbols six names;
safeis byte-identical tooffon a real gcc-O2binary whileidentfolds its.constprop.0; and the name-keyed-lookuprisk is pinned — every name
function_entriesreports still resolves throughlookup_symbol(whatkuna decompile <name>andload functiongo through) onthe hostile fixture and on two real unstripped C++ binaries.
safeset, the no-op-on-real-names list, thedistinctness properties,
identnever deleting a name, the env gate, thehoisted
identchain) plus the RTTI class-name tests kept unchanged across thehoist, plus a new
printctest that the/* renamed from "…" */note cannot beescaped by the raw name.
Gates
Bookkeeping
commas 118 -> 119;
catalog_bytecompatfixture counts 119 -> 120 (x3)and
phase_catalog.jsonregenerated.kuna-base/src/xml.rs);docs/baseline-stages.jsonre-recorded 538 -> 547 keys.docs/history.md. The highest on main is DIV-89 and siblings mayclaim 90/91, so this row and its two references in
architecture.rs/kuna_phases/tests.rsmay need renumbering at merge.Closes #340
🤖 Generated with Claude Code
https://claude.ai/code/session_011KpG7qK6BDFZyPnyo4r1c1