1 Commits
Author SHA1 Message Date
pancake 1e84eb2ae5 Use r_name_check instead of json_utils::valid_flag_name 2026-09-28 17:12:03 +02:00
14 changed files with 150 additions and 75 deletions

No files matched your search

+10 -5
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 `r_bin_demangle`.
One row per query: demangles an ARBITRARY mangled name via `iD`/`iDj`.
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,10 +1269,15 @@ 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.
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.
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.
#### Point-lookup recipes: `name_at` / `section_at` / `string_at`
-7
View File
@@ -26,8 +26,6 @@
// 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
@@ -510,11 +508,6 @@ 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,29 +121,6 @@ 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 &&
+16 -9
View File
@@ -4,6 +4,7 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
#include "json_utils.hpp"
#include <r_util.h>
#include <cctype>
#include <cstdio>
@@ -56,18 +57,24 @@ bool boolean(const nlohmann::json& obj, std::string_view key, bool dflt) {
return dflt;
}
bool valid_flag_name(std::string_view name) {
if (name.empty()) return false;
for (unsigned char c : name) {
const bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '$';
if (!ok) return false;
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);
}
}
return true;
out.push_back('"');
return out;
}
bool valid_type_text(std::string_view type) {
if (type.empty()) return false;
for (unsigned char c : type) {
+1 -6
View File
@@ -31,14 +31,9 @@ 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
// charset; rejecting anything else prevents a name with spaces or command
// separators (`;`, `@`, `|`, backtick, …) from breaking parsing or injecting
// extra r2 commands. Use to gate INSERT/UPDATE on the flags/bookmarks tables.
bool valid_flag_name(std::string_view name);
// True iff `type` is a safe C type string to splice into an r2 `afvt` command
// unquoted: a non-empty string of type-syntax characters only —
+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 RBin demangler
void register_demangle_table(Session& s); // one-row demangler (iD/iDj)
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)
+2 -2
View File
@@ -126,7 +126,7 @@ void register_bookmarks_table(Session& s) {
[](const BookmarkRow& r) { return r.name; },
[be = s.backend_ptr()](BookmarkRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v.c_str())) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
@@ -180,7 +180,7 @@ void register_bookmarks_table(Session& s) {
xsql::set_vtab_error("bookmarks: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name.c_str())) {
xsql::set_vtab_error(
"bookmarks: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
+106 -8
View File
@@ -4,25 +4,93 @@
// This file is licensed under the Human-Origin Source License v1.0.
// See LICENSE.
//
// demangle -- one row for an arbitrary mangled name. Demangling always uses
// r_bin_demangle through the in-process direct::Api.
// 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.
#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 "direct.hpp"
#include "json_utils.hpp"
#include "table_utils.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)
@@ -31,6 +99,18 @@ 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;
@@ -38,13 +118,31 @@ DemangleRow run_demangle(Backend* be, const std::string& mangled,
row.error = "demangle: mangled must not be empty";
return row;
}
auto* api = be->direct();
if (!api) {
row.error = "demangle requires the in-process libr backend";
// 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)";
return row;
}
row.demangled = api->demangle(mangled, lang, row.error);
if (!row.demangled.empty()) row.error.clear();
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)";
}
return row;
}
+3 -3
View File
@@ -127,7 +127,7 @@ void build_flags(Backend* be, std::vector<FlagRow>& rows) {
const std::string name = json_utils::str(sp, "name");
// A space name is spliced into a command, so it gets the same charset gate
// as every other name this project sends to radare2.
if (name.empty() || !json_utils::valid_flag_name(name)) continue;
if (name.empty() || !r_name_check(name.c_str())) continue;
if (json_utils::i64(sp, "count") == 0) continue; // nothing to attribute
// One command, not three: selecting a flagspace is session state, so the
@@ -163,7 +163,7 @@ void register_flags_table(Session& s) {
[](const FlagRow& r) { return r.name; },
[be = s.backend_ptr()](FlagRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v.c_str())) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
@@ -260,7 +260,7 @@ void register_flags_table(Session& s) {
xsql::set_vtab_error("flags: INSERT requires a non-empty name");
return false;
}
if (!json_utils::valid_flag_name(name)) {
if (!r_name_check(name.c_str())) {
xsql::set_vtab_error(
"flags: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
+3 -3
View File
@@ -12,7 +12,7 @@
// radare2's own command handler, seeked to the target address, called
// directly without evaluating its command-chaining grammar (`;`/`@`/`|`/
// backtick) at all. This is an ADDITIONAL layer alongside this table's
// existing grammar/value validation (`valid_flag_name`, `valid_c_signature`,
// existing grammar/value validation (`r_name_check`, `valid_c_signature`,
// the `calling_convention_exists` check on `calltype`) — none replaces the
// others. On every other backend (r2pipe, mock) cmd_noeval reports
// unsupported and the exact same `cmd()` + spliced `@ <addr>` string this
@@ -218,7 +218,7 @@ void register_funcs_table(Session& s) {
[](const FuncRow& r) { return r.name; },
[be = s.backend_ptr()](FuncRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v.c_str())) {
xsql::set_vtab_error(
"funcs: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
@@ -371,7 +371,7 @@ void register_funcs_table(Session& s) {
// Optional best-effort naming; a failed afn does not undo the af above.
if (argc > 1 && !argv[1].is_null()) {
const char* name = argv[1].as_c_str();
if (name && *name && json_utils::valid_flag_name(name)) {
if (name && *name && r_name_check(name.c_str())) {
(void)dispatch_write(be, addr, std::string("afn ") + name);
}
}
+1 -1
View File
@@ -99,7 +99,7 @@ void register_io_maps_table(Session& s) {
std::string name;
if (argc > 3 && !argv[3].is_null()) {
name = argv[3].as_c_str();
if (!name.empty() && !json_utils::valid_flag_name(name.c_str())) {
if (!name.empty() && !r_name_check(name.c_str())) {
xsql::set_vtab_error("io_maps: name must be [A-Za-z0-9._$] (no spaces)");
return false;
}
+4 -4
View File
@@ -49,7 +49,7 @@
// `dispatch_write(be, addr, command)` helper every sibling write-capable
// table (`comments`, `funcs`, `flags`, `bookmarks`, `types`, `patches`)
// already uses. This is an ADDITIONAL layer alongside this table's existing
// grammar validation (`valid_flag_name`/`valid_type_text`), never a
// grammar validation (`r_name_check`/`valid_type_text`), never a
// replacement for it. On the r2pipe backend, `cmd_noeval` reports unsupported
// and the exact `afvn`/`afvt` command text this table always issued runs
// unchanged.
@@ -191,12 +191,12 @@ void register_locals_table(Session& s) {
[](const LocalRow& r) { return r.name; },
[be = s.backend_ptr()](LocalRow& r, const char* v) -> bool {
if (!v || !*v) return false;
if (!json_utils::valid_flag_name(v)) {
if (!r_name_check(v.c_str())) {
xsql::set_vtab_error(
"locals: name must be [A-Za-z0-9._$] (no spaces or r2 metacharacters)");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
@@ -245,7 +245,7 @@ void register_locals_table(Session& s) {
"locals: type contains an r2 metacharacter (@ ; | ` \" $ etc.)");
return false;
}
if (!json_utils::valid_flag_name(r.name)) {
if (!r_name_check(r.name.c_str())) {
xsql::set_vtab_error(
"locals: existing name contains r2 command metacharacters");
return false;
+1 -1
View File
@@ -42,7 +42,7 @@ void register_projects_table(Session& s) {
})
.column_text("name", [](const ProjectRow& r) { return r.name; })
.deletable([be = s.backend_ptr()](ProjectRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("projects: refusing to delete an unsafe/empty project name");
return false;
}
+2 -2
View File
@@ -148,7 +148,7 @@ void register_types_table(Session& s) {
// SQL creates empty struct/union/enum shells and deletes types by name.
// Full declarations use radare2's native td command.
.deletable([be = s.backend_ptr()](TypeRow& r) -> bool {
if (r.name.empty() || !json_utils::valid_flag_name(r.name)) {
if (r.name.empty() || !r_name_check(r.name.c_str())) {
xsql::set_vtab_error("types: refusing to delete an unsafe/empty type name");
return false;
}
@@ -177,7 +177,7 @@ void register_types_table(Session& s) {
const char* kind_c = argv[2].as_c_str();
const std::string name = name_c ? name_c : "";
const std::string kind = kind_c ? kind_c : "";
if (name.empty() || !json_utils::valid_flag_name(name)) {
if (name.empty() || !r_name_check(name.c_str())) {
xsql::set_vtab_error("types: refusing to create an unsafe/empty type name");
return false;
}