#26746·radare2

aa corrupts known main prototype on x86-32, drops argc and shifts argv/envp

Author: andreibelovCreated Sep 18, 2026Updated Sep 18, 2026

Environment

bash
$ date
r2 -v
uname -ms
Fri 18 Sep 01:14:14 BST 2026
radare2 6.2.2 +37545 abi:142 @ linux-x86_64
birth: git.6.2.2 2026-09-17__17:41:32
commit: ad27058877024389292fddf12e1db6e13824ba34
options: gpl -O? cs:5 cl:2 make
Linux x86_64

Description

aa corrupts an already-known main prototype on x86-32.

Before analysis, radare2 has the expected prototype metadata:

r2
[0x00001060]> afcf main
[0x00001060]> tk func.main.args
3
[0x00001060]> tk func.main.arg.0
int,argc
[0x00001060]> tk func.main.arg.1
char **,argv
[0x00001060]> tk func.main.arg.2
char **,envp

After aa, it drops argc and shifts argv and envp one stack word earlier:

r2
[0x00001060]> aa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Name local plt stubs from their got relocs (anal.plt)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Recovering variables (afva@@F)
INFO: Running plugin pre-analysis hooks
INFO: Analyze all functions arguments/locals (afva@@F)
[0x00001060]> s sym.main
[0x0000118d]> afcf main
int main (char **argv, char **envp);
[0x0000118d]> tk func.main.args
2
[0x0000118d]> tk func.main.arg.0
char **,argv
[0x0000118d]> tk func.main.arg.1
char **,envp
[0x0000118d]> tk func.main.arg.2
[0x0000118d]> afvb
arg char ** argv @ ebp+0x8
arg char ** envp @ ebp+0xc

For this standard 32-bit frame, the known ABI prototype requires:

r2
int main (int argc, char **argv, char **envp);
arg int argc @ ebp+0x8
arg char ** argv @ ebp+0xc
arg char ** envp @ ebp+0x10

The recovered locations are therefore associated with prototype arguments 1 and 2 rather than 0 and 1. This looks like an off-by-one mapping between recovered BP-relative argument slots and the pre-existing prototype.

The explicit afva path has a guard that preserves a function with known type metadata:

c
case 'a': // "afva"
    if (fcn) {
        char *type = r_str_newf ("func.%s.ret", fcn->name);
        if (type && sdb_exists (core->anal->sdb_types, type)) {
            // if function type exists
            // do not analize vars if function has a signature
        } else {
            r_anal_function_delete_all_vars (fcn);
            r_core_recover_vars (core, fcn, false);
        }
        ...
    }

cmd_aa() instead calls r_core_recover_vars(core, fcni, true) directly for every function:

c
if (anal_vars) {
    logline (core, 22, "Recovering variables (afva@@F)");

    r_list_foreach_prev (core->anal->fcns, iter, fcni) {
        ...
        r_core_recover_vars (core, fcni, true);
        ...
    }
}

Could this bulk recovery path honor the same signature-preservation rule, or otherwise preserve the known prototype's argument-slot mapping?

Reproducer

c
int main(int argc, char **argv)
{
    return argc + !!argv;
}
bash
gcc -m32 -O0 -fno-omit-frame-pointer test.c -o test
r2 ./test

Then run the commands in the Description section.

Expected result

aa must not replace or reinterpret arguments from the already-known func.main.* prototype. The final prototype and BP-relative variables should remain:

r2
int main (int argc, char **argv, char **envp);
arg int argc @ ebp+0x8
arg char ** argv @ ebp+0xc
arg char ** envp @ ebp+0x10

Actual result

aa replaces the known three-argument prototype with:

r2
int main (char **argv, char **envp);
arg char ** argv @ ebp+0x8
arg char ** envp @ ebp+0xc