CVE-2026-45324
Description
Rizin is a UNIX-like reverse engineering framework and command-line toolset. There is a double free in librz/core/cmd/cmd_search.c:byte_pattern_search() due wrong pointer ownership declared. This vulnerability is fixed by commit 045fff363b42b8a6dda8ad5229c29ec3267e7dbe.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Rizin byte_pattern_search() contains a double free due to wrong pointer ownership, leading to crashes or potential memory corruption.
Vulnerability
A double-free vulnerability exists in Rizin's byte_pattern_search() function within librz/core/cmd/cmd_search.c. The root cause is incorrect pointer ownership handling, which causes a previously freed pointer to be freed again when an error path is taken during search operations. All versions prior to the commit 045fff363b42b8a6dda8ad5229c29ec3267e7dbe are affected [1][2].
Exploitation
An attacker can trigger the double free by providing input that leads to invalid search boundaries (e.g., using an unsupported or malformed search.in region). This causes the function to enter its error handling code, where the free operation is applied to an already freed pointer. No authentication is required, but the attacker must be able to execute search commands with crafted parameters, typically requiring local access or indirect interaction with the tool [2].
Impact
Successful exploitation results in an application crash (denial of service) due to heap corruption. Under certain conditions, the memory corruption could potentially be leveraged for more severe outcomes such as arbitrary code execution, though the low CVSS score (3.3) indicates limited practical likelihood. The primary risk is disruption of the reverse engineering workflow [2].
Mitigation
The vulnerability is fixed in commit 045fff363b42b8a6dda8ad5229c29ec3267e7dbe, released on 2026-05-29 [1]. Users should update Rizin to a build that includes this commit. As a workaround, avoid using search commands with invalid or untrusted boundary values, and validate the search.in parameter against the documented list of valid modes [2]. This CVE is not listed in CISA's Known Exploited Vulnerabilities catalog.
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1045fff363b42Fix double free and reject invalid values for `search.in` (#6327)
6 files changed · +85 −49
librz/core/cbounds.c+56 −42 modified@@ -650,76 +650,90 @@ RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_debug_program(RZ_NO * \brief Returns a list of boundaries (as RzIOMap), based on the selected mode; see [search/analysis/zoom/[in/from/to]] for available modes. * * \param core The RzCore to use - * \param from_key The [search|analysis|zoom].from keyword to use in RzConfig - * \param to_key The [search|analysis|zoom].to keyword to use in RzConfig - * \param in_key The [search|analysis|zoom].in keyword to use in RzConfig + * \param interval The address range of the boundaries. + * \param region The region of the boundaries. Check valid ones with `e search.in=?`. * * \return On success a valid pointer (can be an empty list), otherwise NULL */ -RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_select(RZ_NONNULL RzCore *core, RZ_NONNULL const char *from_key, RZ_NONNULL const char *to_key, RZ_NONNULL const char *in_key) { - rz_return_val_if_fail(core && from_key && to_key && in_key, NULL); - - RzInterval interval; - ut64 from = rz_config_get_i(core->config, from_key); - ut64 to = rz_config_get_i(core->config, to_key); - const char *use_mode = rz_config_get(core->config, in_key); - - interval.addr = from; - interval.size = to - from; - - if (!strcmp(use_mode, "raw") || !strcmp(use_mode, "file")) { +RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries(RZ_NONNULL RzCore *core, RzInterval interval, const char *region) { + rz_return_val_if_fail(core, NULL); + if (!strcmp(region, "raw") || !strcmp(region, "file")) { return rz_core_get_boundaries_raw(core, interval); - } else if (!strcmp(use_mode, "block")) { + } else if (!strcmp(region, "block")) { return rz_core_get_boundaries_block(core, interval); - } else if (!strcmp(use_mode, "dbg.map")) { + } else if (!strcmp(region, "dbg.map")) { return rz_core_get_boundaries_current_debug_map(core, interval); - } else if (!strcmp(use_mode, "io.map")) { + } else if (!strcmp(region, "io.map")) { return rz_core_get_boundaries_current_io_map(core, interval); - } else if (!strcmp(use_mode, "range") || !strcmp(use_mode, "io.maps")) { + } else if (!strcmp(region, "range") || !strcmp(region, "io.maps")) { return rz_core_get_boundaries_all_io_maps(core, interval); - } else if (!strcmp(use_mode, "io.sky")) { + } else if (!strcmp(region, "io.sky")) { return rz_core_get_boundaries_all_io_skyline(core, interval); - } else if (!strcmp(use_mode, "code")) { + } else if (!strcmp(region, "code")) { return rz_core_get_boundaries_code_only(core, interval); - } else if (!strcmp(use_mode, "bin.segment")) { + } else if (!strcmp(region, "bin.segment")) { return rz_core_get_boundaries_current_bin_segment(core, interval); - } else if (!strcmp(use_mode, "bin.section")) { + } else if (!strcmp(region, "bin.section")) { return rz_core_get_boundaries_current_bin_section(core, interval); - } else if (!strcmp(use_mode, "bin.segments")) { + } else if (!strcmp(region, "bin.segments")) { return rz_core_get_boundaries_all_bin_segments(core, interval); - } else if (!strcmp(use_mode, "bin.sections")) { + } else if (!strcmp(region, "bin.sections")) { return rz_core_get_boundaries_all_bin_sections(core, interval); - } else if (!strcmp(use_mode, "analysis.fcn")) { + } else if (!strcmp(region, "analysis.fcn")) { return rz_core_get_boundaries_current_function(core, interval); - } else if (!strcmp(use_mode, "analysis.bb")) { + } else if (!strcmp(region, "analysis.bb")) { return rz_core_get_boundaries_current_function_bb(core, interval); - } else if (!strcmp(use_mode, "dbg.maps")) { + } else if (!strcmp(region, "dbg.maps")) { return rz_core_get_boundaries_all_debug_maps(core, interval); - } else if (!strcmp(use_mode, "dbg.heap")) { + } else if (!strcmp(region, "dbg.heap")) { return rz_core_get_boundaries_debug_heap(core, interval); - } else if (!strcmp(use_mode, "dbg.stack")) { + } else if (!strcmp(region, "dbg.stack")) { return rz_core_get_boundaries_debug_stack(core, interval); - } else if (!strcmp(use_mode, "dbg.program")) { + } else if (!strcmp(region, "dbg.program")) { return rz_core_get_boundaries_debug_program(core, interval); - } else if (rz_str_startswith(use_mode, "dbg.maps.")) { + } else if (rz_str_startswith(region, "dbg.maps.")) { #define PARSE_PERMS(input, mode_name) rz_str_rwx(input + strlen(mode_name)) - int perms = PARSE_PERMS(use_mode, "dbg.maps."); + int perms = PARSE_PERMS(region, "dbg.maps."); return rz_core_get_boundaries_debug_maps(core, interval, perms, perms, false); - } else if (rz_str_startswith(use_mode, "io.sky.")) { - int perms = PARSE_PERMS(use_mode, "io.sky."); + } else if (rz_str_startswith(region, "io.sky.")) { + int perms = PARSE_PERMS(region, "io.sky."); return rz_core_get_boundaries_io_skyline(core, interval, perms, perms); - } else if (rz_str_startswith(use_mode, "io.maps.")) { - int perms = PARSE_PERMS(use_mode, "io.maps."); + } else if (rz_str_startswith(region, "io.maps.")) { + int perms = PARSE_PERMS(region, "io.maps."); return rz_core_get_boundaries_io_maps(core, interval, perms, perms); - } else if (rz_str_startswith(use_mode, "bin.segments.")) { - int perms = PARSE_PERMS(use_mode, "bin.segments."); + } else if (rz_str_startswith(region, "bin.segments.")) { + int perms = PARSE_PERMS(region, "bin.segments."); return rz_core_get_boundaries_bin_segments(core, interval, perms, perms); - } else if (rz_str_startswith(use_mode, "bin.sections.")) { - int perms = PARSE_PERMS(use_mode, "bin.sections."); + } else if (rz_str_startswith(region, "bin.sections.")) { + int perms = PARSE_PERMS(region, "bin.sections."); return rz_core_get_boundaries_bin_sections(core, interval, perms, perms); #undef PARSE_PERMS } - RZ_LOG_ERROR("core: unknown mode '%s' for %s\n", use_mode, in_key); + RZ_LOG_ERROR("core: Invalid mode '%s' for [0x%" PFMT64x ", 0x%" PFMT64x "]\n", + region, interval.addr, interval.size); return NULL; } + +/** + * \brief Returns a list of boundaries (as RzIOMap), based on the selected mode; see [search/analysis/zoom/[in/from/to]] for available modes. + * + * \param core The RzCore to use + * \param from_key The [search|analysis|zoom].from keyword to use in RzConfig + * \param to_key The [search|analysis|zoom].to keyword to use in RzConfig + * \param in_key The [search|analysis|zoom].in keyword to use in RzConfig + * + * \return On success a valid pointer (can be an empty list), otherwise NULL + */ +RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_select(RZ_NONNULL RzCore *core, RZ_NONNULL const char *from_key, RZ_NONNULL const char *to_key, RZ_NONNULL const char *in_key) { + rz_return_val_if_fail(core && from_key && to_key && in_key, NULL); + + RzInterval interval; + ut64 from = rz_config_get_i(core->config, from_key); + ut64 to = rz_config_get_i(core->config, to_key); + const char *use_mode = rz_config_get(core->config, in_key); + + interval.addr = from; + interval.size = to - from; + return rz_core_get_boundaries(core, interval, use_mode); +}
librz/core/cconfig.c+10 −0 modified@@ -2566,7 +2566,17 @@ static ConfigOptDescr search_in_opts[] = { static bool cb_search_in(void *user, void *data) { RzConfigNode *node = (RzConfigNode *)data; + RzCore *core = (RzCore *)user; + RzInterval itv = { + .addr = rz_config_get_i(core->config, "search.from"), + .size = rz_config_get_i(core->config, "search.to") + }; if (node->value[0] != '?') { + RzList *bounds = rz_core_get_boundaries(core, itv, node->value); + if (!bounds) { + return false; + } + rz_list_free(bounds); return true; } else if (strlen(node->value) > 1 && node->value[1] == '?') { rz_cons_printf("Valid values for search.in (depends on .from/.to and io.va):\n");
librz/core/cmd/cmd_search.c+5 −6 modified@@ -1228,6 +1228,7 @@ static void __core_cmd_search_asm_infinite(RzCore *core, const char *arg) { } free(buf); } + rz_list_free(boundaries); } static void __core_cmd_search_asm_byteswap(RzCore *core, int nth) { @@ -1700,6 +1701,10 @@ static RzSearchOpt *setup_search_options(RzCore *core) { } static RzCmdStatus byte_pattern_search(RzCore *core, RZ_OWN RzSearchBytesPattern *pattern, RzCmdStateOutput *state) { + if (!pattern) { + RZ_LOG_ERROR("Failed to parse given pattern.\n"); + return RZ_CMD_STATUS_ERROR; + } RzSearchOpt *search_opts = setup_search_options(core); RzList *hits = NULL; if (!search_opts) { @@ -1709,11 +1714,6 @@ static RzCmdStatus byte_pattern_search(RzCore *core, RZ_OWN RzSearchBytesPattern CMD_SEARCH_BEGIN(); - if (!pattern) { - RZ_LOG_ERROR("Failed to parse given pattern.\n"); - goto error; - } - bool progress = rz_search_opt_get_show_progress(search_opts) != RZ_SEARCH_PROGRESS_DISABLED; if (!rz_search_opt_set_cancel_cb(search_opts, cmd_search_progress_cancel, progress ? state : NULL)) { RZ_LOG_ERROR("code: Failed to setup default search options.\n"); @@ -1731,7 +1731,6 @@ static RzCmdStatus byte_pattern_search(RzCore *core, RZ_OWN RzSearchBytesPattern return cmd_core_handle_search_hits(core, state, hits); error: - rz_search_bytes_pattern_free(pattern); rz_list_free(hits); rz_search_opt_free(search_opts); CMD_SEARCH_END();
librz/include/rz_core.h+1 −0 modified@@ -1204,6 +1204,7 @@ RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_debug_heap(RZ_NONNU RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_debug_stack(RZ_NONNULL RzCore *core, const RzInterval interval); RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_debug_program(RZ_NONNULL RzCore *core, const RzInterval interval); RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries_select(RZ_NONNULL RzCore *core, RZ_NONNULL const char *from_key, RZ_NONNULL const char *to_key, RZ_NONNULL const char *in_key); +RZ_API RZ_OWN RzList /*<RzIOMap *>*/ *rz_core_get_boundaries(RZ_NONNULL RzCore *core, RzInterval interval, const char *region); RZ_API bool rz_core_hack(RzCore *core, const char *op); RZ_API bool rz_core_dump(RzCore *core, const char *file, ut64 addr, ut64 size, int append);
librz/main/rizin.c+1 −1 modified@@ -1003,8 +1003,8 @@ RZ_API int rz_main_rizin(int argc, const char **argv) { if (asmbits) { rz_config_set(r->config, "asm.bits", asmbits); } - rz_config_set(r->config, "search.in", "dbg.map"); // implicit? rz_config_set(r->config, "cfg.debug", "true"); + rz_config_set(r->config, "search.in", "dbg.map"); perms = RZ_PERM_RWX; if (opt.ind >= argc) { RZ_LOG_ERROR("No program given to -d\n");
test/db/cmd/cmd_search_x+12 −0 modified@@ -458,3 +458,15 @@ EXPECT=<<EOF 0x00000076 EOF RUN + +NAME=Heap use after free +FILE== +CMDS=<<EOF +e search.in=section +/x deadbeef +EOF +EXPECT=<<EOF +EOF +EXPECT_ERR=<<EOF +ERROR: core: Invalid mode 'section' for [0x0, 0xffffffffffffffff] +EOF
Vulnerability mechanics
Root cause
"Wrong pointer ownership in `byte_pattern_search()` causes a double free when an invalid `search.in` value leads to a NULL return from boundary resolution."
Attack vector
An attacker who can control the `search.in` configuration value (e.g., by setting it to an invalid string like `"section"`) triggers a code path where `rz_core_get_boundaries_select()` returns `NULL`. In the old code, the caller `byte_pattern_search()` would then jump to the `error` label, which called `rz_search_bytes_pattern_free(pattern)` — but the `pattern` pointer had already been passed as `RZ_OWN` to the function, meaning ownership was already transferred and the caller should not free it again. This results in a double free [CWE-415]. The attack requires local access to set configuration values and run a search command.
Affected code
The double free occurs in `librz/core/cmd/cmd_search.c` in the `byte_pattern_search()` function due to wrong pointer ownership. The patch also modifies `librz/core/cbounds.c` to refactor `rz_core_get_boundaries_select()` into a new `rz_core_get_boundaries()` function, and adds validation in `librz/core/cconfig.c` via the `cb_search_in` callback to reject invalid `search.in` values.
What the fix does
The patch moves the `NULL` check for `pattern` to the top of `byte_pattern_search()` and returns early with `RZ_CMD_STATUS_ERROR` instead of falling through to the `error` label. This eliminates the erroneous `rz_search_bytes_pattern_free(pattern)` call in the error path, fixing the double free. Additionally, the `cb_search_in` callback in `cconfig.c` now validates the `search.in` value by calling `rz_core_get_boundaries()` and rejecting invalid modes, preventing the NULL-return scenario that led to the double free. The refactored `rz_core_get_boundaries()` function centralizes boundary resolution.
Preconditions
- configAttacker must be able to set the `search.in` configuration variable to an invalid value (e.g., `section`).
- inputAttacker must trigger a byte pattern search command (e.g., `/x deadbeef`).
- authAttacker requires local access to the Rizin command-line interface.
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.