Blog
Blog

radare2: Command Injection via unsanitized DWARF arg name in afsv

2026-04-16

TL;DR

A crafted ELF binary can embed radare2 command syntax inside a DWARF DW_TAG_formal_parameter name. When radare2 runs aaa, that name is imported into the type database. Later, afsv / afsvj build a pfq command to print argument values and evaluate the raw name without sanitization.

That means an argument name containing |! can reach local shell command execution when the analyst runs afsvj at a call site. This is not memory corruption. It is a direct command injection.

This post is a blog-formatted rewrite of the technical content in radareorg/radare2 PR #25821.


Target

Product radare2
Verified Revision c8d4c65990b9e0dd74b28f7c676e2aaab11a497d
Trigger Open a crafted ELF, run aaa, then execute afsv or afsvj at a call site
Target Components libr/anal/dwarf_process.c / libr/core/cmd_anal.inc.c
CWE CWE-78: OS Command Injection
Discovery Method Static analysis of the DWARF import path and the command construction path in afsvj, with GPT-5.4-Cyber assistance

The attacker model is simple: the attacker only needs to provide an ELF file that will be opened in radare2. The victim follows a routine analysis flow, runs aaa, then uses afsv or afsvj at a function call site.

afsvj is especially interesting because it is a JSON output command and looks like a harmless display or automation helper. That makes it a plausible sink in wrapper scripts and plugins too.


Overview

The core issue is that a DWARF-derived argument name is first stored as data, then later reused as part of a command string. The vulnerable boundary is not the DWARF parser itself, but the layer that bridges debug info into a display command.

flowchart LR A["Untrusted ELF
DW_TAG_formal_parameter.name"] --> B["DWARF import
aaa"] B --> C["Type DB
func.<name>.arg.N"] C --> D["afsv / afsvj"] D --> E["print_fcn_arg()"] E --> F["r2 command parser"] F --> G["Shell execution"] A:::untrusted C:::storage E:::sink G:::impact B -- "arg_name stored unsanitized" --> C E -- "name embedded into pfq command" --> F classDef untrusted fill:#fff3cd,stroke:#b8860b,color:#000; classDef storage fill:#e7f1ff,stroke:#356fb3,color:#000; classDef sink fill:#fde2e1,stroke:#c0392b,color:#000; classDef impact fill:#f8d7da,stroke:#842029,color:#000;

The bug can be reduced to three steps.

Phase Description
1 The argument name extracted from DWARF is stored in the type DB as-is
2 afsvj reads that value back and passes it into print_fcn_arg()
3 print_fcn_arg() evaluates an r2 command string that still contains the unsanitized name


Source: DWARF to the Type DB

In libr/anal/dwarf_process.c, import_dwarf_function_fallback() walks formal parameters extracted from DWARF and writes them into the type DB as func.<typed_name>.arg.N.

libr/anal/dwarf_process.c:1603-1606 (simplified)
arg_name = var->name ? strdup(var->name) : "argN";
arg_key  = "func.<typed_name>.arg.<N>";
arg_val  = "<type>,<arg_name>";
sdb_set(types, arg_key, arg_val, 0);

The important detail is that the function-side typed_name is sanitized as a C identifier, but arg_name is stored raw. So if the DWARF DW_AT_name contains something like x|!touch /tmp/r2_poc #, that exact string survives in the type DB.

At this point it is still just data storage. The execution boundary appears only when that value is later mixed into the r2 command language.


Sink: Command Evaluation in afsvj

According to the built-in help, afsv prints a function signature using the current register and stack state. Internally, cmd_afsv() resolves the call target when the current instruction is a call or jump, then passes each recovered argument into print_fcn_arg().

libr/core/cmd_anal.inc.c:5206-5223 / 5249-5270 (simplified)
if (current_op_is_call_or_jmp) {
    pcv = aop->jump;
}
fcn_name = resolve_callee_name(pcv);
list = r_core_get_func_args(core, fcn_name);
for each arg in list:
    print_fcn_arg(core, arg->type, arg->name, arg->fmt, arg->src, ...);

The actual sink is print_fcn_arg(), which used to construct a pfq command containing the argument name name and pass it to r_core_cmd_strf().

libr/core/cmd_anal.inc.c:5069-5071 (simplified)
cmd = "pfq " + prefix + fmt + " " + name + " @ " + addr;
res = r_core_cmd_strf(core, cmd);

In radare2, |! is interpreted as the pipe-to-shell operator. That means a malicious argument name containing command metacharacters can turn a display helper into shell execution. For example:

$ x|!touch /tmp/r2_poc #

Once that string is embedded into the command, the trailing @ 0x... portion is commented out, and the effect is essentially touch /tmp/r2_poc.



Proof of Concept

The PR includes a Python script that generates a minimal ELF containing the malicious DWARF data.

  1. Embed the payload into a DW_TAG_formal_parameter name inside the ELF
  2. Run aaa so the DWARF information is imported into the type DB
  3. Move to the call site, set the argument source register, and run afsvj
$ r2 -qc 'aaa; s 0x4000c2; ar rdi=0x4000b0; afsvj' /tmp/dwarf.elf

This is the same invocation style shown in the PR. The ar rdi=... step matters because afsvj evaluates arguments using the current register and stack state. The original PR used open -a Calculator on macOS, but for a proof-of-concept, touch /tmp/r2_poc is enough.

To be precise, aaa alone does not trigger execution. The dangerous sink lives in afsv / afsvj.


Fix

libr/core/cmd_anal.inc.c:5070-5075 (post-fix, simplified)
safe_name = r_str_sanitize_r2(name);
cmd = "pfq " + prefix + fmt + " " + safe_name;
res = r_core_call_str_at(core, addr, cmd);


Impact

Dimension Description
Execution Context Local command execution with the privileges of the user running radare2
Trigger Conditions Open an attacker-controlled ELF, run aaa, then execute afsv / afsvj
Blast Radius Desktop reverse engineering workflows, scripted triage, and helper tools that rely on JSON output
Nature A local command injection reachable from what appears to be a display or inspection command


Timeline

Date Event
2026-04-15 Vulnerability confirmed and fix PR #25821 opened
2026-04-15 Review recommends the r_core_call_str_at()-based fix pattern
2026-04-15 PR merged and the fix lands in master
2026-04-16 This write-up added
1 object(s) selected 1.44 MB
Blog
12:00 PM