]> git.feebdaed.xyz Git - 0xmirror/radare2.git/commitdiff
Add memory leak testsuite using valgrind #tests
authorpancake <pancake@nowsecure.com>
Fri, 19 Dec 2025 23:03:50 +0000 (00:03 +0100)
committerGitHub <noreply@github.com>
Fri, 19 Dec 2025 23:03:50 +0000 (00:03 +0100)
.github/workflows/tcc.yml
binr/r2r/load.c
binr/r2r/r2r.c
binr/r2r/r2r.h
binr/r2r/run.c
libr/main/radare2.c
test/db/leak/open [new file with mode: 0644]
test/unit/test_r2r.c

index 6d0b63943ef75067d7977db3ce537bbde74f59e7..943fd2c96e19089e175d267fb2e3dd595699ffc0 100644 (file)
@@ -227,3 +227,45 @@ jobs:
         export R2R_SKIP_ASM=1
         export R2R_SKIP_ARCHOS=1
         make -j -C test/unit
+
+  leak-check:
+    name: ubuntu-tcc-leaks
+    runs-on: ubuntu-24.04
+    steps:
+    - name: Checkout TinyCC repository
+      run: |
+          git clone https://github.com/mirror/tinycc.git
+          cd tinycc
+          git checkout mob
+          git reset --hard 560526a49dfffef118bcb7fba83c727639ec0a1d
+    - name: Compiling and installing TinyCC
+      working-directory: tinycc
+      run: |
+          sh ./configure --prefix=/usr
+          make -j
+          sudo make install
+    - uses: actions/checkout@v6
+    - name: Checkout our Testsuite Binaries
+      uses: actions/checkout@v6
+      with:
+          repository: radareorg/radare2-testbins
+          path: test/bins
+    - name: Install dependencies
+      run: |
+        sudo apt update --assume-yes
+        sudo apt-get --assume-yes install gperf wheel setuptools valgrind || true
+        sudo pip install r2pipe --break-system-packages
+    - name: Configure, build and install
+      env:
+          CC: tcc
+      run: |
+          ./configure --prefix=/usr --with-compiler=tcc
+          make -j
+          sudo make install
+    - name: Run leak tests
+      env:
+        PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
+      run: |
+        r2 -v
+        r2r -v
+        r2r test/db/leak
index 49f9cecb25b1376705fc0cc25e03595419f08e30..362fdb01b0162eab9ec4b2bf28b0629631a112f0 100644 (file)
@@ -504,6 +504,10 @@ static R2RTest *r2r_test_new(R2RTestType type, const char *path, void *specific_
                test->json_test = specific_test;
                test->json_test->load_plugins = load_plugins;
                break;
+       case R2R_TEST_TYPE_LEAK:
+               test->cmd_test = specific_test;
+               test->cmd_test->load_plugins = load_plugins;
+               break;
        case R2R_TEST_TYPE_FUZZ:
                break;
        }
@@ -524,6 +528,9 @@ R_API void r2r_test_free(R2RTest *test) {
        case R2R_TEST_TYPE_JSON:
                r2r_json_test_free (test->json_test);
                break;
+       case R2R_TEST_TYPE_LEAK:
+               r2r_cmd_test_free (test->cmd_test);
+               break;
        case R2R_TEST_TYPE_FUZZ:
                break;
        }
@@ -559,6 +566,8 @@ static R2RTestFrom test_type_for_path(const char *path) {
                res.type = R2R_TEST_TYPE_ASM;
        } else if (strstr (path, R_SYS_DIR "json" R_SYS_DIR)) {
                res.type = R2R_TEST_TYPE_JSON;
+       } else if (strstr (path, R_SYS_DIR "leak" R_SYS_DIR)) {
+               res.type = R2R_TEST_TYPE_LEAK;
        } else {
                if (strstr (path, R_SYS_DIR "extras" R_SYS_DIR)) {
                        res.load_plugins = true;
@@ -573,7 +582,7 @@ static R2RTestFrom test_type_for_path(const char *path) {
        return res;
 }
 
-static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool skip_json_tests) {
+static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool skip_json_tests, bool skip_leak_tests) {
 #if WANT_V35 == 0
        R2RTestToSkip v35_tests_to_skip[] = {
                { "asm", "arm.v35_64" },
@@ -635,7 +644,7 @@ static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool
                                continue;
                        }
                        char *subpath = r_file_new (path, subname, NULL);
-                       ret = database_load (db, subpath, depth - 1, skip_json_tests);
+                       ret = database_load (db, subpath, depth - 1, skip_json_tests, skip_leak_tests);
                        free (subpath);
                        if (!ret) {
                                break;
@@ -656,6 +665,9 @@ static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool
        if (skip_json_tests && tff.type == R2R_TEST_TYPE_JSON) {
                return true;
        }
+       if (skip_leak_tests && tff.type == R2R_TEST_TYPE_LEAK) {
+               return true;
+       }
        switch (tff.type) {
        case R2R_TEST_TYPE_CMD:
                {
@@ -699,6 +711,20 @@ static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool
                        RVecR2RJsonTestPtr_free (json_tests);
                        break;
                }
+       case R2R_TEST_TYPE_LEAK:
+               {
+                       RVecR2RCmdTestPtr *cmd_tests = r2r_load_cmd_test_file (path);
+                       if (!cmd_tests) {
+                               return false;
+                       }
+                       R2RCmdTest **it;
+                       R_VEC_FOREACH (cmd_tests, it) {
+                               R2RTest *test = r2r_test_new (R2R_TEST_TYPE_LEAK, pooled_path, *it, tff.load_plugins);
+                               RVecR2RTestPtr_push_back (&db->tests, &test);
+                       }
+                       RVecR2RCmdTestPtr_free (cmd_tests);
+                       break;
+               }
        case R2R_TEST_TYPE_FUZZ:
                // shouldn't come here, fuzz tests are loaded differently
                break;
@@ -707,8 +733,8 @@ static bool database_load(R2RTestDatabase *db, const char *path, int depth, bool
        return true;
 }
 
-R_API bool r2r_test_database_load(R2RTestDatabase *db, const char *path, bool skip_json_tests) {
-       return database_load (db, path, 4, skip_json_tests);
+R_API bool r2r_test_database_load(R2RTestDatabase *db, const char *path, bool skip_json_tests, bool skip_leak_tests) {
+       return database_load (db, path, 4, skip_json_tests, skip_leak_tests);
 }
 
 static void database_load_fuzz_file(R2RTestDatabase *db, const char *path, const char *file) {
index 27f1bd024bd2fd478a4e9398fd3fa0cd06d02d84..6bd9779a42a7c0516b2a3e21054e5099fda35876 100644 (file)
@@ -82,10 +82,12 @@ static void parse_skip(const char *arg) {
                r_sys_setenv ("R2R_SKIP_FUZZ", "1");
        } else if (strstr (arg, "json")) {
                r_sys_setenv ("R2R_SKIP_JSON", "1");
+       } else if (strstr (arg, "leak")) {
+               r_sys_setenv ("R2R_SKIP_LEAK", "1");
        } else if (strstr (arg, "asm")) {
                r_sys_setenv ("R2R_SKIP_ASM", "1");
        } else {
-               R_LOG_ERROR ("Invalid -s argument: @arch @unit @cmd @fuzz @json @asm");
+               R_LOG_ERROR ("Invalid -s argument: @arch @asm @cmd @fuzz @json @leak @unit");
        }
 }
 
@@ -97,6 +99,7 @@ static void helpvars(int workers_count) {
                "R2R_SKIP_UNIT=0     # do not run the unit tests\n"
                "R2R_SKIP_CMD=0      # do not run the cmds tests\n"
                "R2R_SKIP_ASM=0      # do not run the rasm2 tests\n"
+               "R2R_SKIP_LEAK=0     # do not run the leak tests (valgrind)\n"
                "R2R_JOBS=%d       # maximum parallel jobs\n"
                "R2R_TIMEOUT=%d    # timeout after 1 minute (60 * 60)\n"
                "R2R_OFFLINE=0       # same as passing -u\n"
@@ -129,7 +132,7 @@ static int help(bool verbose, int workers_count) {
                        " -v           show version\n"
                        "\n");
                helpvars (workers_count);
-               printf ("\nSupported test types: @asm @json @unit @fuzz @arch @cmd\nOS/Arch for archos tests: %s\n", getarchos ());
+               printf ("\nSupported test types: @arch @asm @cmd @fuzz @json @leak @unit\nOS/Arch for archos tests: %s\n", getarchos ());
        }
        return 1;
 }
@@ -528,6 +531,7 @@ static bool r2r_state_init(R2RState *state, R2ROptions *opt) {
        state->run_config.skip_asm = r_sys_getenv_asbool ("R2R_SKIP_ASM");
        state->run_config.skip_json = r_sys_getenv_asbool ("R2R_SKIP_JSON");
        state->run_config.skip_fuzz = r_sys_getenv_asbool ("R2R_SKIP_FUZZ");
+       state->run_config.skip_leak = r_sys_getenv_asbool ("R2R_SKIP_LEAK");
        state->run_config.json_test_file = opt->json_test_file? opt->json_test_file: JSON_TEST_FILE_DEFAULT;
        state->run_config.timeout_ms = (opt->timeout_sec > UT64_MAX / 1000)? UT64_MAX: opt->timeout_sec * 1000;
        state->verbose = opt->verbose;
@@ -570,8 +574,12 @@ static int r2r_load_tests(R2RState *state, R2ROptions *opt, int arg_ind, int arg
        if (skip_json_tests) {
                R_LOG_INFO ("Skipping json tests because jq is not available");
        }
+       bool skip_leak_tests = !r2r_check_valgrind_available ();
+       if (skip_leak_tests) {
+               R_LOG_INFO ("Skipping leak tests because valgrind is not available");
+       }
        if (arg_ind >= argc) {
-               if (!r2r_test_database_load (state->db, "db", skip_json_tests)) {
+               if (!r2r_test_database_load (state->db, "db", skip_json_tests, skip_leak_tests)) {
                        R_LOG_ERROR ("Failed to load tests from ./db");
                        return -1;
                }
@@ -605,10 +613,16 @@ static int r2r_load_tests(R2RState *state, R2ROptions *opt, int arg_ind, int arg
                                arg = "db/json";
                        } else if (!strcmp (arg, "dasm")) {
                                arg = "db/asm";
-                       } else if (!strcmp (arg, "cmds")) {
+                       } else if (!strcmp (arg, "cmds") || !strcmp (arg, "cmd")) {
                                arg = "db";
+                       } else if (!strcmp (arg, "asm")) {
+                               arg = "db/asm";
+                       } else if (!strcmp (arg, "arch")) {
+                               arg = "db/archos";
+                       } else if (!strcmp (arg, "leak")) {
+                               arg = "db/leak";
                        } else {
-                               arg_str = r_str_newf ("db/%s", arg + 1);
+                               arg_str = r_str_newf ("db/%s", arg);
                                arg = arg_str;
                        }
                }
@@ -636,7 +650,7 @@ static int r2r_load_tests(R2RState *state, R2ROptions *opt, int arg_ind, int arg
                        return grc? grc: 1; // Signal special exit
                }
                char *tf = r_file_abspath_rel (cwd, arg);
-               if (!tf || !r2r_test_database_load (state->db, tf, skip_json_tests)) {
+               if (!tf || !r2r_test_database_load (state->db, tf, skip_json_tests, skip_leak_tests)) {
                        R_LOG_ERROR ("Failed to load tests from \"%s\"", tf);
                        free (tf);
                        free (arg_str);
@@ -694,6 +708,12 @@ static void test_result_to_json(PJ *pj, R2RTestResultInfo *result) {
                pj_s (pj, "json");
                pj_ks (pj, "cmd", test->json_test->cmd);
                break;
+       case R2R_TEST_TYPE_LEAK:
+               pj_s (pj, "leak");
+               if (test->cmd_test->name.value) {
+                       pj_ks (pj, "name", test->cmd_test->name.value);
+               }
+               break;
        case R2R_TEST_TYPE_FUZZ:
                pj_s (pj, "fuzz");
                pj_ks (pj, "file", test->fuzz_test->file);
@@ -872,6 +892,13 @@ static void print_result_diff(R2RRunConfig *config, R2RTestResultInfo *result) {
                        printf ("-- stderr\n%s\n", result->proc_out->err);
                        printf ("-- exit status: " Color_RED "%d" Color_RESET "\n", result->proc_out->ret);
                        break;
+               case R2R_TEST_TYPE_LEAK:
+                       r2r_run_leak_test (config, result->test->cmd_test, print_runner, NULL);
+                       printf ("-- valgrind output\n%s\n", result->proc_out->out);
+                       if (result->proc_out->err) {
+                               printf ("-- stderr\n%s\n", result->proc_out->err);
+                       }
+                       break;
                case R2R_TEST_TYPE_ASM:
                case R2R_TEST_TYPE_JSON:
                        // diffing not yet implemented for those tests
index 0f346078ca3d5c6b93556edf2cd82264ee59ef69..89dbe4b0f537b69b241e9286e2203b1e9ef3f7ba 100644 (file)
@@ -124,7 +124,8 @@ typedef enum r2r_test_type_t {
        R2R_TEST_TYPE_CMD,
        R2R_TEST_TYPE_ASM,
        R2R_TEST_TYPE_JSON,
-       R2R_TEST_TYPE_FUZZ
+       R2R_TEST_TYPE_FUZZ,
+       R2R_TEST_TYPE_LEAK
 } R2RTestType;
 
 typedef struct r2r_test_from_t {
@@ -159,6 +160,7 @@ typedef struct r2r_run_config_t {
        bool skip_fuzz;
        bool skip_asm;
        bool skip_json;
+       bool skip_leak;
 } R2RRunConfig;
 
 typedef struct r2r_process_output_t {
@@ -223,7 +225,7 @@ R_API RVecR2RJsonTestPtr *r2r_load_json_test_file(const char *file);
 
 R_API R2RTestDatabase *r2r_test_database_new(void);
 R_API void r2r_test_database_free(R2RTestDatabase *db);
-R_API bool r2r_test_database_load(R2RTestDatabase *db, const char *path, bool skip_json_tests);
+R_API bool r2r_test_database_load(R2RTestDatabase *db, const char *path, bool skip_json_tests, bool skip_leak_tests);
 R_API bool r2r_test_database_load_fuzz(R2RTestDatabase *db, const char *path);
 
 typedef struct r2r_subprocess_t R2RSubprocess;
@@ -243,6 +245,7 @@ R_API void r2r_process_output_free(R2RProcessOutput *out);
 R_API R2RProcessOutput *r2r_run_cmd_test(R2RRunConfig *config, R2RCmdTest *test, R2RCmdRunner runner, void *user);
 R_API bool r2r_check_cmd_test(R2RProcessOutput *out, R2RCmdTest *test);
 R_API bool r2r_check_jq_available(void);
+R_API bool r2r_check_valgrind_available(void);
 R_API R2RProcessOutput *r2r_run_json_test(R2RRunConfig *config, R2RJsonTest *test, R2RCmdRunner runner, void *user);
 R_API bool r2r_check_json_test(R2RProcessOutput *out, R2RJsonTest *test);
 R_API R2RAsmTestOutput *r2r_run_asm_test(R2RRunConfig *config, R2RAsmTest *test);
@@ -250,6 +253,8 @@ R_API bool r2r_check_asm_test(R2RAsmTestOutput *out, R2RAsmTest *test);
 R_API void r2r_asm_test_output_free(R2RAsmTestOutput *out);
 R_API R2RProcessOutput *r2r_run_fuzz_test(R2RRunConfig *config, const char *file, R2RCmdRunner runner, void *user);
 R_API bool r2r_check_fuzz_test(R2RProcessOutput *out);
+R_API R2RProcessOutput *r2r_run_leak_test(R2RRunConfig *config, R2RCmdTest *test, R2RCmdRunner runner, void *user);
+R_API bool r2r_check_leak_test(R2RProcessOutput *out, R2RCmdTest *test);
 
 R_API void r2r_test_free(R2RTest *test);
 R_API char *r2r_test_name(R2RTest *test);
index 4e3e1625fb6b761413888add0107c267762f00cb..f5933a4a7932b7653a5b9471c5078ea7aa499a13 100644 (file)
@@ -205,8 +205,7 @@ error:
 }
 
 R_API R2RSubprocess *r2r_subprocess_start(
-       const char *file, const char *args[], size_t args_size,
-       const char *envvars[], const char *envvals[], size_t env_size) {
+       const char *file, const char *args[], size_t args_size, const char *envvars[], const char *envvals[], size_t env_size) {
        char **argv = calloc (args_size + 1, sizeof (char *));
        R2RSubprocess *proc = NULL;
        HANDLE stdin_read = NULL, stdout_write = NULL, stderr_write = NULL;
@@ -266,9 +265,7 @@ R_API R2RSubprocess *r2r_subprocess_start(
        start_info.dwFlags |= STARTF_USESTDHANDLES;
 
        LPWSTR env = override_env (envvars, envvals, env_size);
-       if (!CreateProcessA (NULL, cmdline,
-               NULL, NULL, TRUE, CREATE_UNICODE_ENVIRONMENT, env,
-               NULL, &start_info, &proc_info)) {
+       if (!CreateProcessA (NULL, cmdline, NULL, NULL, TRUE, CREATE_UNICODE_ENVIRONMENT, env, NULL, &start_info, &proc_info)) {
                free (env);
                R_LOG_ERROR ("CreateProcess failed: %#x", (int)GetLastError ());
                goto error;
@@ -617,8 +614,7 @@ static inline void dup_retry(int fds[2], int n, int b) {
 }
 
 R_API R2RSubprocess *r2r_subprocess_start(
-       const char *file, const char *args[], size_t args_size,
-       const char *envvars[], const char *envvals[], size_t env_size) {
+       const char *file, const char *args[], size_t args_size, const char *envvars[], const char *envvals[], size_t env_size) {
        int stdin_pipe[2] = { -1, -1 };
        int stdout_pipe[2] = { -1, -1 };
        int stderr_pipe[2] = { -1, -1 };
@@ -867,9 +863,9 @@ R_API R2RProcessOutput *r2r_subprocess_drain(R2RSubprocess *proc) {
        R_RETURN_VAL_IF_FAIL (proc, NULL);
        if (proc->lock && r_th_lock_enter (proc->lock)) {
                R2RProcessOutput *out = R_NEW0 (R2RProcessOutput);
-// XXX for some reason strdup handles memory better than drain_nofree
-//             out->out = r_strbuf_drain_nofree (&proc->out);
-//             out->err = r_strbuf_drain_nofree (&proc->err);
+               // XXX for some reason strdup handles memory better than drain_nofree
+               //              out->out = r_strbuf_drain_nofree (&proc->out);
+               //              out->err = r_strbuf_drain_nofree (&proc->err);
                out->out = strdup (r_strbuf_get (&proc->out));
                out->err = strdup (r_strbuf_get (&proc->err));
                out->ret = proc->ret;
@@ -904,11 +900,11 @@ cleanup_without_vector:
                // This prevents double frees when r2r_subprocess_drain has been called
                if (proc->out.ptr) {
                        r_strbuf_fini (&proc->out);
-               //      r_strbuf_init (&proc->out); // Reinitialize to avoid issues with subsequent r_strbuf_fini
+                       //      r_strbuf_init (&proc->out); // Reinitialize to avoid issues with subsequent r_strbuf_fini
                }
                if (proc->err.ptr) {
                        r_strbuf_fini (&proc->err);
-               //      r_strbuf_init (&proc->err); // Reinitialize to avoid issues with subsequent r_strbuf_fini
+                       //      r_strbuf_init (&proc->err); // Reinitialize to avoid issues with subsequent r_strbuf_fini
                }
                // Release the process lock before freeing it
                r_th_lock_leave (proc->lock);
@@ -950,8 +946,7 @@ R_API void r2r_process_output_free(R2RProcessOutput *out) {
        free (out);
 }
 
-static R2RProcessOutput *subprocess_runner(const char *file, const char *args[], size_t args_size,
-       const char *envvars[], const char *envvals[], size_t env_size, ut64 timeout_ms, void *user) {
+static R2RProcessOutput *subprocess_runner(const char *file, const char *args[], size_t args_size, const char *envvars[], const char *envvals[], size_t env_size, ut64 timeout_ms, void *user) {
        R2RSubprocess *proc = r2r_subprocess_start (file, args, args_size, envvars, envvals, env_size);
        if (!proc) {
                return NULL;
@@ -1091,11 +1086,16 @@ static R2RProcessOutput *run_r2_test(R2RRunConfig *config, ut64 timeout_ms, int
        if (extra_env) {
                char *kv;
                r_list_foreach (extra_env, it, kv) {
-                       char *equal = strstr (kv, "=");
-                       if (equal) {
-                               *equal = 0;
-                               r_list_append (envvars, (void *)kv);
-                               r_list_append (envvals, (void *) (equal + 1));
+                       char *dup = strdup (kv);
+                       if (dup) {
+                               char *equal = strstr (dup, "=");
+                               if (equal) {
+                                       *equal = 0;
+                                       r_list_append (envvars, (void *)dup);
+                                       r_list_append (envvals, (void *) (equal + 1));
+                               } else {
+                                       free (dup);
+                               }
                        }
                }
        }
@@ -1163,8 +1163,7 @@ R_API R2RProcessOutput *r2r_run_cmd_test(R2RRunConfig *config, R2RCmdTest *test,
        }
        int repeat = test->repeat.value;
        const ut64 timeout_ms = test->timeout.set? test->timeout.value * 1000: config->timeout_ms;
-       R2RProcessOutput *out = run_r2_test (config, timeout_ms, repeat,
-               test->cmds.value, files, extra_args, extra_env, test->load_plugins, runner, user);
+       R2RProcessOutput *out = run_r2_test (config, timeout_ms, repeat, test->cmds.value, files, extra_args, extra_env, test->load_plugins, runner, user);
        r_list_free (extra_args);
        r_list_free (files);
        r_list_free (extra_env);
@@ -1227,6 +1226,15 @@ R_API bool r2r_check_jq_available(void) {
        return invalid_detected && valid_detected;
 }
 
+R_API bool r2r_check_valgrind_available(void) {
+       char *valgrind_bin = r_file_path ("valgrind");
+       if (!valgrind_bin) {
+               return false;
+       }
+       free (valgrind_bin);
+       return true;
+}
+
 R_API R2RProcessOutput *r2r_run_json_test(R2RRunConfig *config, R2RJsonTest *test, R2RCmdRunner runner, void *user) {
        RList *files = r_list_new ();
        r_list_push (files, (void *)config->json_test_file);
@@ -1404,6 +1412,262 @@ R_API bool r2r_check_fuzz_test(R2RProcessOutput *out) {
        return out && out->ret == 0 && out->out && out->err && !out->timeout;
 }
 
+// Parse valgrind LEAK SUMMARY to check for memory leaks
+// Returns true if no leaks detected (test passes)
+static bool parse_valgrind_leak_summary(const char *valgrind_out) {
+       if (!valgrind_out) {
+               return false;
+       }
+       // Find LEAK SUMMARY section
+       const char *leak_summary = strstr (valgrind_out, "LEAK SUMMARY:");
+       if (!leak_summary) {
+               // If no LEAK SUMMARY found, consider it a failure
+               return false;
+       }
+       // Look for the three leak categories (ignore "still reachable")
+       // Pattern: "definitely lost: X bytes"
+       // Pattern: "indirectly lost: X bytes"
+       // Pattern: "possibly lost: X bytes"
+       ut64 definitely_lost = 0;
+       ut64 indirectly_lost = 0;
+       ut64 possibly_lost = 0;
+       // Extract "definitely lost" value
+       const char *p = strstr (leak_summary, "definitely lost:");
+       if (p) {
+               // Parse the number before " bytes"
+               p += 16; // strlen ("definitely lost:")
+               while (*p == ' ') {
+                       p++;
+               }
+               definitely_lost = r_num_math (NULL, p);
+       }
+       // Extract "indirectly lost" value
+       p = strstr (leak_summary, "indirectly lost:");
+       if (p) {
+               p += 16; // strlen ("indirectly lost:")
+               while (*p == ' ') {
+                       p++;
+               }
+               indirectly_lost = r_num_math (NULL, p);
+       }
+       // Extract "possibly lost" value
+       p = strstr (leak_summary, "possibly lost:");
+       if (p) {
+               p += 14; // strlen ("possibly lost:")
+               while (*p == ' ') {
+                       p++;
+               }
+               possibly_lost = r_num_math (NULL, p);
+       }
+       // Test passes only if all three are 0
+       return definitely_lost == 0 && indirectly_lost == 0 && possibly_lost == 0;
+}
+
+// Run r2 test wrapped with valgrind
+// Similar to run_r2_test but wraps the command with valgrind
+static R2RProcessOutput *run_r2_test_with_valgrind(R2RRunConfig *config, ut64 timeout_ms, int repeat, const char *cmds, RList *files, RList *extra_args, RList *extra_env, bool load_plugins, R2RCmdRunner runner, void *user) {
+       RList *args = r_list_new ();
+       RList *envvars = r_list_new ();
+       RList *envvals = r_list_new ();
+       // Add valgrind arguments
+       r_list_append (args, (void *)"--leak-check=full");
+#if 0
+       r_list_append (args, (void *)"--show-leak-kinds=all");
+       r_list_append (args, (void *)"--track-origins=yes");
+#endif
+       r_list_append (args, (void *)config->r2_cmd);
+#if 0
+       r_list_append (args, (void *)"-escr.utf8=0");
+       r_list_append (args, (void *)"-escr.color=0");
+       r_list_append (args, (void *)"-escr.interactive=0");
+#endif
+       if (!load_plugins) {
+               r_list_append (args, (void *)"-NN");
+       }
+       RListIter *it;
+       void *extra_arg, *file_arg;
+       if (extra_args) {
+               r_list_foreach (extra_args, it, extra_arg) {
+                       r_list_append (args, extra_arg);
+               }
+       }
+       // THIS FLAG LEAKS r_list_append (args, (void *)"-Qc");
+       r_list_append (args, (void *)"-qc");
+#if R2__WINDOWS__
+       char *wcmds = convert_win_cmds (cmds);
+       r_list_append (args, wcmds);
+#else
+       r_list_append (args, (void *)cmds);
+#endif
+       r_list_foreach (files, it, file_arg) {
+               r_list_append (args, file_arg);
+       }
+#if R2__WINDOWS__
+       r_list_append (envvars, (void *)"ANSICON");
+       r_list_append (envvals, (void *)"1");
+#endif
+       if (!load_plugins) {
+               r_list_append (envvars, (void *)"R2_NOPLUGINS");
+               r_list_append (envvals, (void *)"1");
+       }
+       if (extra_env) {
+               char *kv;
+               r_list_foreach (extra_env, it, kv) {
+                       char *dup = strdup (kv);
+                       if (dup) {
+                               char *equal = strstr (dup, "=");
+                               if (equal) {
+                                       *equal = 0;
+                                       r_list_append (envvars, (void *)dup);
+                                       r_list_append (envvals, (void *) (equal + 1));
+                               } else {
+                                       free (dup);
+                               }
+                       }
+               }
+       }
+       size_t args_size, env_size;
+       const char **argv = rlist_to_argv (args, &args_size);
+       const char **envk = rlist_to_argv (envvars, &env_size);
+       const char **envv = rlist_to_argv (envvals, &env_size);
+
+       // Run valgrind instead of radare2 directly
+       R2RProcessOutput *out = runner ("valgrind", argv, args_size, envk, envv, env_size, timeout_ms, user);
+
+#if R2__WINDOWS__
+       free (wcmds);
+#endif
+       free (argv);
+       free (envk);
+       free (envv);
+       r_list_free (args);
+       r_list_free (envvars);
+       r_list_free (envvals);
+       return out;
+}
+
+// Run a leak test with valgrind
+// Returns process output with valgrind output in stdout/stderr
+R_API R2RProcessOutput *r2r_run_leak_test(R2RRunConfig *config, R2RCmdTest *test, R2RCmdRunner runner, void *user) {
+// Check if we're on Linux
+#if !R2__UNIX__
+       R2RProcessOutput *out = R_NEW0 (R2RProcessOutput);
+       out->ret = 1;
+       out->out = strdup ("Leak tests only run on Linux");
+       out->err = strdup ("");
+       return out;
+#else
+       // Check if valgrind is available
+       char *valgrind_bin = r_file_path ("valgrind");
+       if (!valgrind_bin) {
+               R2RProcessOutput *out = R_NEW0 (R2RProcessOutput);
+               out->ret = 1;
+               out->out = strdup ("valgrind not found");
+               out->err = strdup ("");
+               return out;
+       }
+       free (valgrind_bin);
+
+       // Run the test with valgrind --leak-check=full
+       // We need to build a valgrind command with the same args as the normal cmd test
+       RList *extra_args = test->args.value? r_str_split_duplist (test->args.value, " ", true): NULL;
+       RList *files = test->file.value? r_str_split_duplist (test->file.value, "\n", true): NULL;
+       RListIter *it;
+       RListIter *tmpit;
+       RList *extra_env = NULL;
+       char *token;
+
+       if (extra_args) {
+               r_list_foreach_safe (extra_args, it, tmpit, token) {
+                       if (!*token) {
+                               r_list_delete (extra_args, it);
+                       }
+               }
+       }
+       if (!files) {
+               files = r_list_newf (free);
+               r_list_append (files, strdup ("-"));
+       }
+       r_list_foreach_safe (files, it, tmpit, token) {
+               if (!*token) {
+                       r_list_delete (files, it);
+               }
+       }
+       if (r_list_empty (files)) {
+               files->free = NULL;
+               r_list_push (files, "-");
+       }
+       if (test->env.value) {
+               extra_env = r_str_split_duplist (test->env.value, ";", true);
+       }
+
+       const ut64 timeout_ms = test->timeout.set? test->timeout.value * 1000: config->timeout_ms;
+
+       // Run with valgrind wrapping
+       R2RProcessOutput *out = run_r2_test_with_valgrind (config, timeout_ms, 1, test->cmds.value, files, extra_args, extra_env, test->load_plugins, runner, user);
+
+       r_list_free (extra_args);
+       r_list_free (files);
+       r_list_free (extra_env);
+       return out;
+#endif
+}
+
+// Check if a leak test passed (no memory leaks)
+R_API bool r2r_check_leak_test(R2RProcessOutput *out, R2RCmdTest *test) {
+       if (!out) {
+               return false;
+       }
+
+       // Combine stdout and stderr for leak checking
+       RStrBuf *combined = r_strbuf_new (NULL);
+       if (out->out) {
+               r_strbuf_append (combined, out->out);
+       }
+       if (out->err) {
+               r_strbuf_append (combined, out->err);
+       }
+       char *full_output = r_strbuf_drain (combined);
+
+       // Check for leaks in valgrind output
+       bool leak_check = parse_valgrind_leak_summary (full_output);
+       free (full_output);
+
+       if (!leak_check) {
+               return false;
+       }
+
+       // Also check normal cmd test expectations (EXPECT, EXPECT_ERR, etc)
+       // For leak tests run with valgrind:
+       // - out->out contains the actual program's stdout
+       // - out->err contains valgrind's diagnostic output (with ==PID== lines)
+       const char *expect_out = test->expect.value;
+       const char *expect_err = test->expect_err.value;
+       const char *regexp_out = test->regexp_out.value;
+       const char *regexp_err = test->regexp_err.value;
+
+       // Check EXPECT output (actual program output, not valgrind diagnostics)
+       if (expect_out && out->out && strcmp (out->out, expect_out) != 0) {
+               return false;
+       }
+
+       // Check EXPECT_ERR (for leak tests, stderr is valgrind output, not checked by default)
+       if (expect_err && out->err && strcmp (out->err, expect_err) != 0) {
+               return false;
+       }
+
+       // Check REGEXP_OUT
+       if (regexp_out && out->out && !r_regex_match (regexp_out, "e", out->out)) {
+               return false;
+       }
+
+       // Check REGEXP_ERR
+       if (regexp_err && out->err && !r_regex_match (regexp_err, "e", out->err)) {
+               return false;
+       }
+       return !out->timeout;
+}
+
 R_API char *r2r_test_name(R2RTest *test) {
        switch (test->type) {
        case R2R_TEST_TYPE_CMD:
@@ -1417,6 +1681,11 @@ R_API char *r2r_test_name(R2RTest *test) {
                return r_str_newf ("<json> %s", r_str_get (test->json_test->cmd));
        case R2R_TEST_TYPE_FUZZ:
                return r_str_newf ("done"); // <fuzz> %s", shortpath (test->path));
+       case R2R_TEST_TYPE_LEAK:
+               if (test->cmd_test->name.value) {
+                       return r_str_newf ("<leak> %s", test->cmd_test->name.value);
+               }
+               return strdup ("<leak> <unnamed>");
        }
        return NULL;
 }
@@ -1436,6 +1705,7 @@ R_API int r2r_test_needsabi(R2RTest *test) {
        case R2R_TEST_TYPE_ASM:
        case R2R_TEST_TYPE_JSON:
        case R2R_TEST_TYPE_FUZZ:
+       case R2R_TEST_TYPE_LEAK:
                break;
        }
        return 0;
@@ -1451,6 +1721,8 @@ R_API bool r2r_test_broken(R2RTest *test) {
                return test->json_test->broken;
        case R2R_TEST_TYPE_FUZZ:
                return false;
+       case R2R_TEST_TYPE_LEAK:
+               return test->cmd_test->broken.value;
        }
        return false;
 }
@@ -1625,6 +1897,34 @@ R_API R2RTestResultInfo *r2r_run_test(R2RRunConfig *config, R2RTest *test) {
                        ret->run_failed = !out;
                }
                break;
+       case R2R_TEST_TYPE_LEAK:
+               if (config->skip_leak) {
+                       success = true;
+                       ret->run_failed = false;
+               } else {
+                       R2RCmdTest *cmd_test = test->cmd_test;
+                       const char *require = cmd_test->require.value;
+                       if (!require_check (require)) {
+                               R_LOG_WARN ("Skipping because of %s", require);
+                               success = true;
+                               ret->run_failed = false;
+                               break;
+                       }
+#if WANT_V35 == 0
+                       if (cmd_test->args.value && strstr (cmd_test->args.value, "arm.v35")) {
+                               R_LOG_WARN ("Skipping test because it requires arm.v35");
+                               success = true;
+                               ret->run_failed = false;
+                               break;
+                       }
+#endif
+                       R2RProcessOutput *out = r2r_run_leak_test (config, cmd_test, subprocess_runner, NULL);
+                       success = r2r_check_leak_test (out, cmd_test);
+                       ret->proc_out = out;
+                       ret->timeout = out? out->timeout: false;
+                       ret->run_failed = !out;
+               }
+               break;
        }
        ret->time_elapsed = r_time_now_mono () - start_time;
        bool broken = r2r_test_broken (test);
@@ -1651,6 +1951,7 @@ R_API void r2r_test_result_info_free(R2RTestResultInfo *result) {
                case R2R_TEST_TYPE_CMD:
                case R2R_TEST_TYPE_JSON:
                case R2R_TEST_TYPE_FUZZ:
+               case R2R_TEST_TYPE_LEAK:
                        r2r_process_output_free (result->proc_out);
                        break;
                case R2R_TEST_TYPE_ASM:
index f98bc1f58920f020f4f756ec37df795277b5b9f9..536e492f1b2673e56f64376adbb2c396740c7be8 100644 (file)
@@ -285,7 +285,8 @@ static int main_help(int line) {
                                                                                        " R2_IGNABI       ignore abiversion field from the radare (be even more careful)\n"
                                                                                        " R2_MAGICPATH    %s/" R2_SDB_MAGIC "\n"
                                                                                        " R2_NOPLUGINS    do not load r2 shared plugins\n",
-                       dirPrefix, dirPrefix);
+                       dirPrefix,
+                       dirPrefix);
                r_strbuf_append (sb, " R2_HISTORY      ${XDG_CACHE_DIR:=~/.cache/radare2}/history\n");
                r_strbuf_append (sb, " R2_RCFILE       ~/.radare2rc (user preferences, batch script)\n" // TOO GENERIC
                                " R2_CURL         set to '1' to use system curl program instead of r2 apis\n");
@@ -352,9 +353,9 @@ static int main_print_var(const char *var_name) {
        } r2_vars[] = {
                { "R2_VERSION", R2_VERSION },
                { "R2_VERSION_ABI", R2_ABIVERSION_STRING },
-               { "R2_VERSION_MAJOR", STRINGIFY(R2_VERSION_MAJOR) },
-               { "R2_VERSION_MINOR", STRINGIFY(R2_VERSION_MINOR) },
-               { "R2_VERSION_PATCH", STRINGIFY(R2_VERSION_PATH) },
+               { "R2_VERSION_MAJOR", STRINGIFY (R2_VERSION_MAJOR) },
+               { "R2_VERSION_MINOR", STRINGIFY (R2_VERSION_MINOR) },
+               { "R2_VERSION_PATCH", STRINGIFY (R2_VERSION_PATH) },
                { "R2_ABIVERSION", R2_ABIVERSION_STRING },
                { "R2_PREFIX", r2prefix },
                { "R2_MAGICPATH", magicpath },
@@ -689,10 +690,10 @@ static void mainr2_init(RMainRadare2 *mr) {
        mr->baddr = UT64_MAX;
        mr->seek = UT64_MAX;
        mr->perms = R_PERM_RX;
-       mr->cmds = r_list_new ();
-       mr->evals = r_list_new ();
-       mr->files = r_list_new ();
-       mr->prefiles = r_list_new ();
+       mr->cmds = r_list_newf (free);
+       mr->evals = r_list_newf (free);
+       mr->files = r_list_newf (free);
+       mr->prefiles = r_list_newf (free);
 }
 
 static void mainr2_fini(RMainRadare2 *mr) {
diff --git a/test/db/leak/open b/test/db/leak/open
new file mode 100644 (file)
index 0000000..7c17e37
--- /dev/null
@@ -0,0 +1,20 @@
+NAME=leak opening malloc
+FILE=malloc://32
+CMDS=<<EOF
+?e hello
+EOF
+EXPECT=<<EOF
+hello
+EOF
+RUN
+
+NAME=leak opening elf
+FILE=bins/elf/ip-riscv
+CMDS=<<EOF
+?e hello
+EOF
+EXPECT=<<EOF
+hello
+EOF
+RUN
+
index 32afd6bab00e53ff9166b71368f1022583c82645..30daebd3bd31679200e3b4524d73e4ec26bb1214 100644 (file)
@@ -13,7 +13,7 @@
 
 bool test_r2r_database_load_cmd(void) {
        R2RTestDatabase *db = r2r_test_database_new ();
-       database_load (db, FILENAME, 1, false);
+       database_load (db, FILENAME, 1, false, false);
 
        mu_assert_eq (RVecR2RTestPtr_length (&db->tests), 4, "tests count");
 
@@ -57,7 +57,7 @@ bool test_r2r_database_load_cmd(void) {
 
 bool test_r2r_fix(void) {
        R2RTestDatabase *db = r2r_test_database_new ();
-       database_load (db, FILENAME, 1, false);
+       database_load (db, FILENAME, 1, false, false);
 
        RVecR2RTestResultInfoPtr results;
        RVecR2RTestResultInfoPtr_init (&results);