1 Commits
Author SHA1 Message Date
pancake f748970e45 Use the r_bin_demangle native api instead of iD command 2026-09-28 17:42:22 +02:00
7 changed files with 46 additions and 134 deletions

No files matched your search

+5 -10
View File
@@ -1245,7 +1245,7 @@ its own, the identical trade-off `rop_gadgets` already made for its own
libr).
#### `demangle` (read-only)
One row per query: demangles an ARBITRARY mangled name via `iD`/`iDj`.
One row per query: demangles an ARBITRARY mangled name via `r_bin_demangle`.
Distinct from `funcs.name`/`imports.name`, which already demangle
automatically (gated on `bin.demangle`) for symbols radare2 already knows
from the loaded binary — `demangle` answers the same question for a string
@@ -1269,15 +1269,10 @@ what `lang` says — verified live that an unrecognized `lang` string does not
abort demangling, it just falls through to auto-detection. `mangled` is NOT
charset-restricted the way `assemble`/`search_asm`'s `asm` is: MSVC manglings
use `?`/`@`/`$` throughout, which a disassembly-text allowlist would reject.
Instead this table dispatches through `Backend::cmd_noeval`, seeked to a
fixed anchor (0, unobserved by `iD`'s handler either way — same shape as
`types`' addr-less `td`/`t-` dispatch), falling back to the
`json_utils::r2_quote`-wrapped plain command on every backend that reports
cmd_noeval unsupported. Verified live that the injection risk is real: `iDj
cxx foo; ?e INJECTED` spliced through a plain `Backend::cmd()` call DOES
execute the injected `?e` as a second command, while `cmd_noeval` reaches
`iD`'s argument parser with the whole string, `;` included, treated as
opaque data.
The in-process backend passes the mangled name directly to `r_bin_demangle`,
with the current `RBinFile` and `bin.lang` default. This preserves arbitrary
input without command parsing. A backend without an in-process RBin API
reports an error for this table.
#### Point-lookup recipes: `name_at` / `section_at` / `string_at`
+7
View File
@@ -26,6 +26,8 @@
// That TU boundary is what forces the row struct out of its table's anonymous
// namespace and into this header: a row struct lives beside its table until the
// table is ported, and moves here when a second TU has to build it.
// The demangle method also uses this boundary so arbitrary symbol text reaches
// RBin directly, without passing through the r2 command parser.
#pragma once
@@ -508,6 +510,11 @@ class Api {
virtual void binary_info(std::vector<BinaryRow>& out) = 0;
virtual std::string file_path() = 0;
virtual std::string engine_version() = 0;
// Demangle an arbitrary symbol using the current RBinFile and bin.lang
// setting. Empty result means radare2 could not demangle the name; error
// receives the same diagnostic as iDj in that case.
virtual std::string demangle(const std::string& mangled,
const std::string& lang, std::string& error) = 0;
virtual bool entry_address(int64_t& out) = 0;
virtual void relocs_scan(std::vector<RelocRow>& out) = 0;
virtual void type_members_scan(std::vector<MemberRow>& out) = 0;
+23
View File
@@ -121,6 +121,29 @@ class LibrApi : public Api {
return 0;
}
std::string demangle(const std::string& mangled,
const std::string& lang, std::string& error) override {
std::lock_guard lock(mu_);
error = "Cannot demangle string";
if (!core_ || !core_->bin) return {};
const char* selected_lang = lang.empty()
? r_config_get(core_->config, "bin.lang") : lang.c_str();
if ((!selected_lang || !*selected_lang) && lang.empty()) {
error = "Set the language in `e bin.lang` or pass a second argument";
}
RBinFile temporary_file = {};
RBinFile* file = core_->bin->cur;
if (!file) {
temporary_file.rbin = core_->bin;
file = &temporary_file;
}
char* result = r_bin_demangle(file, selected_lang, mangled.c_str(), 0, false);
if (!result) return {};
std::string demangled(result);
free(result);
return demangled;
}
bool function_is_start(int64_t addr) override {
std::lock_guard lock(mu_);
return core_ && core_->anal &&
+1 -16
View File
@@ -56,22 +56,7 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt) {
return dflt;
}
std::string r2_quote(std::string_view s) {
std::string out;
out.reserve(s.size() + 2);
out.push_back('"');
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
default: out.push_back(c);
}
}
out.push_back('"');
return out;
}
bool valid_flag_name(std::string_view name) {
if (name.empty()) return false;
+1 -1
View File
@@ -31,7 +31,7 @@ int64_t i64_addr(const nlohmann::json& obj, int64_t dflt = 0);
// Quote a string for use as a shell-ish r2 command argument. r2 itself is
// not a shell, but spaces, quotes and backslashes need escaping in CC args.
std::string r2_quote(std::string_view s);
// True iff `name` is safe to splice into an r2 flag command unquoted: a
// non-empty string of [A-Za-z0-9._$] only. This is roughly r2's own flag-name
+1 -1
View File
@@ -59,7 +59,7 @@ void register_class_methods_table(Session& s);
void register_registers_table(Session& s); // static per-arch register profile (arpj)
void register_relocs_table(Session& s); // relocation table (irj)
void register_libs_table(Session& s); // linked library list (ilj)
void register_demangle_table(Session& s); // one-row demangler (iD/iDj)
void register_demangle_table(Session& s); // one-row RBin demangler
void register_assemble_table(Session& s); // one-row assembler (pa)
void register_search_asm_table(Session& s); // assemble-then-search (/a)
void register_read_cstr_table(Session& s); // arbitrary-address cstring read (pszj)
+8 -106
View File
@@ -4,93 +4,25 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
//
// demangle -- one-row-per-query demangler over radare2's `iD`/`iDj` (verified
// against the CURRENT radare2 source, `libr/core/cmd_info.inc.c`'s
// `cmd_info_demangle` + `libr/bin/demangle.c`'s `r_bin_demangle`/
// `r_bin_demangle_type`, not assumed from a citation). This closes a REAL
// gap: `funcs.name` and `imports.name` already demangle automatically (via
// `r_core_anal_fcn_name`/`r_bin_demangle` respectively, gated on the
// `bin.demangle` config) for symbols radare2 already knows about from the
// loaded binary, but there was no SQL surface to demangle an ARBITRARY
// string that isn't necessarily any table's row.
//
// The `mangled` hidden input is NOT charset-restricted the way
// `assemble`/`search_asm`'s `asm` input is (see tables_assemble.cpp):
// real-world manglings routinely use characters a disassembly-text
// allowlist would reject -- MSVC uses `?`/`@`/`$` throughout
// (`?foo@@YAHXZ`), Rust and Itanium C++ use `$`-escaped substitutions
// heavily. So this table uses the SAME structural no-eval dispatch
// (Backend::cmd_noeval) `types`' `td` already pilots
// for an addr-less command with a genuinely unrestrictable free-text
// argument -- verified live (not assumed) that the injection risk is real:
// `iDj cxx foo; ?e INJECTED` through a plain `Backend::cmd()` splice DOES
// execute the injected `?e` as a second command (radare2's own `;`
// chaining), while `cmd_noeval` reaches `iD`'s single argument parser with
// the whole string, `;` included, treated as opaque data.
//
// `iD`/`iDj` carries no address in its own grammar at all (same shape as
// `td`/`t-`), so this dispatches seeked to a fixed anchor (0, same constant
// choice as `table_utils::kTypeSystemAnchorAddr` -- kept as a small local
// copy rather than reusing that name directly, since this table has nothing
// to do with the type system; the anchor value is arbitrary and unobserved
// by `iD`'s handler either way, verified against `cmd_info_demangle`, which
// reads only its own `lang`/`text` arguments, never `core->addr`).
//
// `lang` is passed through UNVALIDATED against radare2's own known-language
// list: verified live that `r_bin_demangle_type()` returning
// `R_BIN_LANG_NONE` for an unrecognized string does NOT abort the call --
// `cmd_info_demangle` still invokes `r_bin_demangle(bf, lang, text, ...)`,
// which runs its OWN auto-detection from the mangled text's prefix
// (`_Z`/`__Z`/`?`/...) regardless of what `lang` said. So a caller-supplied
// `lang` is always advisory, never validated client-side to match; this
// table simply forwards it and lets radare2 do what it already does.
//
// `iDj`'s own JSON always echoes `lang` back VERBATIM (the input string,
// not radare2's actually-auto-detected type) -- confirmed live with
// `iDj bogus _ZN3Foo3barEv`, which still demangled successfully via
// auto-detection while echoing `"lang":"bogus"`. This table does not
// surface that echo at all (mangled/lang are pure hidden inputs, never
// duplicated as output columns, matching byte_search's own
// pattern-is-hidden-only convention) precisely to avoid presenting that
// potentially-misleading echo as if it were the resolved demangler.
//
// No JSON mode exists for the empty-`text` or unparseable-argument cases
// either -- `iDj` still emits a well-formed `{"lang":...,"mangled":...,
// "error":"..."}` object in every failure mode exercised (empty text,
// non-mangled text, unrecognized lang), so `error` is always populated
// exactly when `demangled` is not, with no separate "malformed command"
// case to handle.
//
// Command-path-only, no C-API producer: a single demangle call has no N+1
// or full-scan cost shape to collapse (same "one full-scan-equivalent
// command, no per-row cost" reasoning `bininfo`/`rop_gadgets` already
// documented for themselves), so both backends share the identical
// command-based implementation and cannot disagree.
// demangle -- one row for an arbitrary mangled name. Demangling always uses
// r_bin_demangle through the in-process direct::Api.
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <r2xsql/session.hpp>
#include <r2xsql/backend.hpp>
#include <xsql/vtable.hpp>
#include "tables.hpp"
#include "json_utils.hpp"
#include "table_utils.hpp"
#include "direct.hpp"
namespace r2xsql {
namespace {
// Arbitrary, unobserved by `iD`'s handler (see file header) -- kept as a
// small local constant rather than reusing table_utils::kTypeSystemAnchorAddr
// so this table reads as self-contained, not borrowed from the type system.
constexpr int64_t kDemangleAnchorAddr = 0;
struct DemangleRow {
std::string demangled; // empty iff demangling failed (never a legitimate
// empty symbol name)
@@ -99,18 +31,6 @@ struct DemangleRow {
enum Col { kDemangled = 0, kError = 1, kMangled = 2, kLang = 3 };
// Seeked-to-a-fixed-anchor no-eval dispatch for `iDj`, mirroring
// table_utils::dispatch_type_write's shape exactly (try cmd_noeval first,
// fall back to the r2_quote-wrapped plain command on every backend that
// reports it unsupported) -- a local copy, not a shared call, since this
// table's anchor and command shape are its own (see file header).
std::string dispatch_demangle(Backend* be, const std::string& noeval_command,
const std::string& fallback_command) {
std::string out;
if (be->cmd_noeval(kDemangleAnchorAddr, noeval_command, &out)) return out;
return be->cmd(fallback_command);
}
DemangleRow run_demangle(Backend* be, const std::string& mangled,
const std::string& lang) {
DemangleRow row;
@@ -118,31 +38,13 @@ DemangleRow run_demangle(Backend* be, const std::string& mangled,
row.error = "demangle: mangled must not be empty";
return row;
}
// Two-arg form when lang is given ("iDj <lang> <mangled>"), single-arg
// form otherwise ("iDj <mangled>", radare2 defaults lang from its own
// bin.lang config) -- matches iD's own grammar (see file header); this
// table never synthesizes a placeholder lang token to keep the shapes
// separate, since args="" lang would misparse a mangled name containing
// its own leading space.
const std::string plain =
lang.empty() ? ("iDj " + mangled) : ("iDj " + lang + " " + mangled);
const std::string out = dispatch_demangle(be, plain, json_utils::r2_quote(plain));
nlohmann::json j = nlohmann::json::parse(out, nullptr, false);
if (j.is_discarded() || !j.is_object()) {
row.error = "demangle: radare2 returned no result (iDj produced no "
"parseable output)";
auto* api = be->direct();
if (!api) {
row.error = "demangle requires the in-process libr backend";
return row;
}
if (j.contains("error") && j["error"].is_string()) {
row.error = j["error"].get<std::string>();
return row;
}
row.demangled = json_utils::str(j, "demangled");
if (row.demangled.empty()) {
row.error = "demangle: radare2 produced neither a demangled name nor an "
"error (unexpected iDj shape)";
}
row.demangled = api->demangle(mangled, lang, row.error);
if (!row.demangled.empty()) row.error.clear();
return row;
}