// SPDX-FileCopyrightText: 2016-2020 n4x0r <kalianon2816@gmail.com>
// SPDX-FileCopyrightText: 2016-2020 soez <soez@amn3s1a.com>
// SPDX-FileCopyrightText: 2016-2020 pancake <pancake@nopcode.org>
// SPDX-License-Identifier: LGPL-3.0-only

#include <rz_cmd.h>
#include <rz_core.h>
#include <rz_config.h>
#include <rz_types.h>

#include <rz_debug.h>
#include <math.h>

#include "core_private.h"

#ifdef HEAP64
#include "linux_heap_glibc64.h"
#else
#include "linux_heap_glibc.h"
#endif

static GH(RTcache) * GH(tcache_new)(RzCore *core);
static bool GH(tcache_read)(RzCore *core, GHT tcache_start, GH(RTcache) * tcache);
static int GH(tcache_get_count)(GH(RTcache) * tcache, int index);
static GHT GH(tcache_get_entry)(GH(RTcache) * tcache, int index);
void GH(rz_heap_chunk_free)(RzHeapChunkListItem *item);
static bool GH(is_arena)(RzCore *core, GHT m_arena, GHT m_state);
RZ_API void GH(tcache_free)(GH(RTcache) * tcache);
void GH(print_heap_chunk_simple)(RzCore *core, GHT chunk, const char *status, PJ *pj);

/**
 * \brief Find the address of a given symbol
 * \param core RzCore Pointer to the Rizin's core
 * \param path Pointer to the binary path in which to look for the symbol
 * \param sym_name Pointer to the symbol's name to search for
 * \return address
 *
 * Used to find the address of a given symbol inside a binary
 *
 * TODO: Stop using deprecated functions like rz_bin_cur
 */
static GHT GH(get_va_symbol)(RzCore *core, const char *path, const char *sym_name) {
	GHT vaddr = GHT_MAX;
	RzBin *bin = core->bin;
	RzBinFile *current_bf = rz_bin_cur(bin);
	void **iter;
	RzBinSymbol *s;

	RzBinOptions opt;
	rz_bin_options_init(&opt, -1, 0, 0, false);
	opt.obj_opts.elf_load_sections = rz_config_get_b(core->config, "elf.load.sections");
	opt.obj_opts.elf_checks_sections = rz_config_get_b(core->config, "elf.checks.sections");
	opt.obj_opts.elf_checks_segments = rz_config_get_b(core->config, "elf.checks.segments");

	RzBinFile *libc_bf = rz_bin_open(bin, path, &opt);
	if (!libc_bf) {
		return vaddr;
	}

	RzBinObject *o = rz_bin_cur_object(bin);
	RzPVector *syms = o ? (RzPVector *)rz_bin_object_get_symbols(o) : NULL;
	rz_pvector_foreach (syms, iter) {
		s = *iter;
		if (!strcmp(s->name, sym_name)) {
			vaddr = s->vaddr;
			break;
		}
	}

	rz_bin_file_delete(bin, libc_bf);
	rz_bin_file_set_cur_binfile(bin, current_bf);
	return vaddr;
}

static inline GHT GH(get_next_pointer)(RzCore *core, GHT pos, GHT next) {
	return (core->dbg->glibc_version < 232) ? next : (GHT)((pos >> 12) ^ next);
}

static GHT GH(get_main_arena_with_symbol)(RzCore *core, RzDebugMap *map) {
	rz_return_val_if_fail(core && map, GHT_MAX);
	GHT base_addr = map->addr;
	rz_return_val_if_fail(base_addr != GHT_MAX, GHT_MAX);

	GHT main_arena = GHT_MAX;
	GHT off = GHT_MAX;
	char *path = rz_str_dup(map->name);
	if (path && rz_file_exists(path)) {
		off = GH(get_va_symbol)(core, path, "main_arena");
		if (off != GHT_MAX) {
			main_arena = base_addr + off;
			goto beach;
		}
		RzBinObject *o = rz_bin_cur_object(core->bin);
		RzBinInfo *info = o ? (RzBinInfo *)rz_bin_object_get_info(o) : NULL;
		if (!strcmp(info->arch, "x86") && info->bits == 64 &&
			// Assumes that the vaddr of LOAD0 is 0x0
			(off = GH(get_va_symbol)(core, path, "mallopt")) != GHT_MAX) {
			// This code looks for the following instructions:
			//     mov edx, 1
			//     (lock) cmpxchg dword [<main_arena_addr>], edx
			//
			// The instructions should be part of the following C
			// code in mallopt():
			//     __libc_lock_lock (av->mutex);
			ut64 mallopt_addr = base_addr + off;
			ut8 bytes[200] = { 0 };
			rz_io_read_at(core->io, mallopt_addr, bytes, sizeof(bytes));
			const ut8 mov[] = { 0xba, 0x1, 0x0, 0x0, 0x0 };
			const ut8 cmpxchg[] = { 0x0f, 0xb1, 0x15 };
			const ut8 *mov_ptr = rz_mem_mem(bytes, sizeof(bytes), mov, sizeof(mov));
			if (!mov_ptr ||
				sizeof(bytes) - (mov_ptr - bytes) <
					sizeof(mov) + 1 /* LOCK */ + sizeof(cmpxchg) + sizeof(ut32)) {
				goto beach;
			}
			const ut8 *cmpxchg_ptr = mov_ptr + sizeof(mov);
			if (*cmpxchg_ptr == 0xf0) { // LOCK prefix
				cmpxchg_ptr++;
			}
			if (memcmp(cmpxchg_ptr, cmpxchg, sizeof(cmpxchg))) {
				goto beach;
			}
			const ut8 *main_arena_off_ptr = cmpxchg_ptr + sizeof(cmpxchg);
			ut32 main_arena_off = rz_read_le32(main_arena_off_ptr);
			ut64 rip_addr = (main_arena_off_ptr + sizeof(ut32) - bytes) + mallopt_addr;
			main_arena = rip_addr + main_arena_off;
			goto beach;
		}
	}
beach:
	free(path);
	return main_arena;
}

static ut8 *GH(get_glibc_banner)(RzCore *core, const char *section_name,
	const char *libc_path) {
	RzPVector *sections = NULL;
	RzBin *bin = core->bin;
	RzBinFile *current_bf = rz_bin_cur(bin);

	void **iter;
	ut8 *buf = NULL;
	ut8 *buf_parse = NULL;
	ut8 *ret_buf = NULL;
	RzBinSection *rz_section = NULL;

	RzBinOptions opt;
	rz_bin_options_init(&opt, -1, 0, 0, false);
	opt.obj_opts.elf_load_sections = rz_config_get_b(core->config, "elf.load.sections");
	opt.obj_opts.elf_checks_sections = rz_config_get_b(core->config, "elf.checks.sections");
	opt.obj_opts.elf_checks_segments = rz_config_get_b(core->config, "elf.checks.segments");

	RzBinFile *libc_buf = rz_bin_open(bin, libc_path, &opt);
	if (!libc_buf) {
		goto cleanup;
	}

	sections = rz_bin_object_get_sections(libc_buf->o);
	if (!sections) {
		goto cleanup;
	}

	rz_pvector_foreach (sections, iter) {
		rz_section = *iter;
		if (strncmp(rz_section->name, section_name, strlen(section_name))) {
			continue;
		}
		buf = calloc(rz_section->size, 1);
		GHT read_size = rz_buf_read_at(libc_buf->buf, rz_section->paddr, buf, rz_section->size);
		if (read_size != rz_section->size) {
			free(buf);
			buf = NULL;
			goto cleanup;
		}
		buf_parse = (ut8 *)rz_mem_mem((const ut8 *)buf, rz_section->size, (const ut8 *)"GNU C Library", strlen("GNU C Library"));
		ret_buf = (ut8 *)rz_str_dup((char *)buf_parse);
		break;
	}

cleanup:
	free(buf);
	rz_pvector_free(sections);
	rz_bin_file_delete(bin, libc_buf);
	rz_bin_file_set_cur_binfile(bin, current_bf);
	return ret_buf;
}

/**
 * \brief Find the glibc version using string search
 * \param core RzCore Pointer to the Rizin's core
 * \param libc_path Pointer to the libc binary path.
 * \param banner_start Pointer to the libc banner start which contains libc details.
 * \return version
 *
 * Used to find the glibc version for the provided libc path or libc banner.
 *
 */

RZ_API double GH(rz_get_glibc_version)(RzCore *core, const char *libc_path, ut8 *banner_start) {
	double version = 0.0;
	ut8 *libc_ro_section = NULL;

	if (!banner_start) {
		libc_ro_section = GH(get_glibc_banner)(core, ".rodata", libc_path);
		if (!libc_ro_section) {
			return version;
		}
	}

	const char *pattern = "release version (\\d.\\d\\d)";
	RzRegex *re = rz_regex_new(pattern, RZ_REGEX_EXTENDED | RZ_REGEX_CASELESS, 0, NULL);
	if (!re) {
		return version;
	}
	RzPVector *matches = rz_regex_match_first(re, (const char *)libc_ro_section,
		RZ_REGEX_ZERO_TERMINATED, 0, RZ_REGEX_DEFAULT);
	if (rz_pvector_empty(matches)) {
		goto cleanup;
	}

	RzRegexMatch *match = rz_pvector_at(matches, 1);
	if (!match) {
		goto cleanup;
	}
	char *version_str = rz_str_ndup((const char *)libc_ro_section + match->start, match->len);
	if (!version_str) {
		goto cleanup;
	}
	version = strtod(version_str, NULL);
	if (version != 0) {
		RZ_LOG_INFO("libc version %.2f identified from .rodata banner\n", version);
	}

	free(version_str);
cleanup:
	rz_pvector_free(matches);
	rz_regex_free(re);
	free(libc_ro_section);
	return version;
}

static GHT GH(read_val)(RzCore *core, const void *src, bool is_big_endian) {
	if (SZ == RZ_SYS_BITS_16) {
		return rz_read_ble16(src, is_big_endian);
	} else if (SZ == RZ_SYS_BITS_32) {
		return rz_read_ble32(src, is_big_endian);
	} else {
		return rz_read_ble64(src, is_big_endian);
	}
}

/**
 * \brief Fill the glibc tcache entries.
 * \param core RzCore Pointer to the Rizin's core
 * \param tcache Pointer to the tcache struct.
 * \return RzList pointer for the list of tcache bins.
 *
 * Used to fill the tcache bins for the specific tcache.
 *
 */

static RZ_BORROW RzList /*<RzList *>*/ *GH(fill_tcache_entries)(RzCore *core, GH(RTcache) * tcache) {
	RzList *tcache_bins_list = rz_list_newf((RzListFree)GH(rz_heap_bin_free));
	if (!tcache_bins_list) {
		goto error;
	}

	// Use rz_tcache struct to get bins
	for (int i = 0; i < TCACHE_MAX_BINS; i++) {
		int count = GH(tcache_get_count)(tcache, i);
		GHT entry = GH(tcache_get_entry)(tcache, i);

		RzHeapBin *bin = RZ_NEW0(RzHeapBin);
		if (!bin) {
			goto error;
		}
		bin->type = rz_str_dup("Tcache");
		bin->bin_num = i;
		bin->chunks = rz_list_newf((RzListFree)GH(rz_heap_chunk_free));
		if (!bin->chunks) {
			GH(rz_heap_bin_free)
			(bin);
			goto error;
		}
		rz_list_append(tcache_bins_list, bin);
		if (count <= 0) {
			continue;
		}
		bin->fd = (ut64)(entry - GH(HDR_SZ));
		// get first chunk
		RzHeapChunkListItem *chunk = RZ_NEW0(RzHeapChunkListItem);
		if (!chunk) {
			GH(rz_heap_bin_free)
			(bin);
			goto error;
		}
		chunk->addr = (ut64)(entry - GH(HDR_SZ));
		rz_list_append(bin->chunks, chunk);

		if (count <= 1) {
			continue;
		}

		// get rest of the chunks
		GHT tcache_fd = entry;
		GHT tcache_tmp = GHT_MAX;
		for (size_t n = 1; n < count; n++) {
			int r = rz_io_nread_at(core->io, tcache_fd, (ut8 *)&tcache_tmp, sizeof(GHT));
			if (r <= 0) {
				goto error;
			}
			tcache_tmp = GH(get_next_pointer)(core, tcache_fd, read_le(&tcache_tmp));
			chunk = RZ_NEW0(RzHeapChunkListItem);
			if (!chunk) {
				goto error;
			}
			// the base address of the chunk = address - 2 * PTR_SIZE
			chunk->addr = (ut64)(tcache_tmp - GH(HDR_SZ));
			rz_list_append(bin->chunks, chunk);
			tcache_fd = tcache_tmp;
		}
	}
	return tcache_bins_list;

error:
	rz_list_free(tcache_bins_list);
	return NULL;
}

static void GH(print_tcache)(RzCore *core, RzList /*<RzList *>*/ *bins, PJ *pj, const GHT tid) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	RzHeapBin *bin;
	RzListIter *iter;

	if (tid != 0) {
		rz_cons_printf("---------- Tcachebins for thread %d ----------", (int)tid);
		rz_cons_newline();
	}

	rz_list_foreach (bins, iter, bin) {
		if (!bin) {
			continue;
		}
		if (!bin->chunks->length) {
			continue;
		}
		if (!pj) {
			rz_cons_printf("%s", bin->type);
			rz_cons_printf("_bin[");
			PRINTF_BA("%02zu", (size_t)bin->bin_num);
			rz_cons_printf("]: Items:");
			PRINTF_BA("%2d", rz_list_length(bin->chunks));
			rz_cons_newline();
		} else {
			pj_o(pj);
			pj_ks(pj, "bin_type", "tcache");
			pj_kn(pj, "bin_num", bin->bin_num);
			pj_ka(pj, "chunks");
		}
		RzHeapChunkListItem *pos;
		RzListIter *iter2;
		RzList *chunks = bin->chunks;
		rz_list_foreach (chunks, iter2, pos) {
			if (!pj) {
				rz_cons_printf(" -> ");
			}
			GH(print_heap_chunk_simple)
			(core, pos->addr, NULL, pj);
			if (!pj) {
				rz_cons_newline();
			}
		}
		if (bin->message) {
			PRINTF_RA("%s\n", bin->message);
		}
		if (pj) {
			pj_end(pj);
			pj_end(pj);
		}
	}
	rz_list_free(bins);
}

/**
 * \brief Checks if the binary is using tcache from glibc.
 * \param core RzCore Pointer to the Rizin's core
 * \return True if binary uses tcache else False.
 *
 * Find if the binary uses tcache using glibc version parsing.
 */

static bool GH(is_tcache)(RzCore *core) {
	// NOTE This method of resolving libc fails in the following cases:
	// 1. libc shared object file does not have version number
	// 2. if another map has `libc-` in its absolute path
	char *fp = NULL;
	double v = 0;
	RzDebugMap *map = NULL;
	bool is_libc_map = false;

	if (core->dbg->is_glibc_resolved) {
		return true;
	}

	if (rz_config_get_b(core->config, "cfg.debug")) {
		RzListIter *iter;
		rz_debug_map_sync(core->dbg);
		RzRegex *re = rz_regex_new(".*libc[.-]", RZ_REGEX_EXTENDED | RZ_REGEX_CASELESS, 0, NULL);
		rz_list_foreach (core->dbg->maps, iter, map) {
			// In case the binary is named *libc-*
			if (strncmp(map->name, core->bin->file, strlen(map->name)) == 0) {
				continue;
			}
			fp = strstr(map->name, "libc-");
			if (fp) {
				is_libc_map = true;
				break;
			}
			RzRegexStatus ret_status = rz_regex_match(re, map->name, RZ_REGEX_ZERO_TERMINATED,
				0, RZ_REGEX_DEFAULT);
			if (ret_status > 0) {
				is_libc_map = true;
				break;
			}
		}
		rz_regex_free(re);
	} else {
		int tcv = rz_config_get_i(core->config, "dbg.glibc.tcache");
		RZ_LOG_WARN("core: dbg.glibc.tcache has been set to %i\n", tcv);
		return tcv != 0;
	}
	if (fp) {

		// In case there is string `libc-` in path actual libc go to last occurrence of `libc-`
		while (strstr(fp + 1, "libc-") != NULL) {
			fp = strstr(fp + 1, "libc-");
		}

		v = rz_num_get_float(NULL, fp + 5);
	} else if (map && is_libc_map) {
		v = GH(rz_get_glibc_version)(core, map->file, NULL);
		if (v) {
			core->dbg->is_glibc_resolved = true;
		}
	}

	core->dbg->glibc_version = (int)round((v * 100));
	return (v > 2.25);
}

static GHT GH(tcache_chunk_size)(RzCore *core, GHT brk_start) {
	GHT sz = 0;

	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk) {
		return sz;
	}
	rz_io_read_at(core->io, brk_start, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	sz = (cnk->size >> 3) << 3; // clear chunk flag
	return sz;
}

static void GH(update_arena_with_tc)(GH(RzHeap_MallocState_tcache) * cmain_arena, MallocState *main_arena) {
	int i = 0;
	main_arena->mutex = cmain_arena->mutex;
	main_arena->flags = cmain_arena->flags;
	for (i = 0; i < BINMAPSIZE; i++) {
		main_arena->binmap[i] = cmain_arena->binmap[i];
	}
	main_arena->have_fast_chunks = cmain_arena->have_fast_chunks;
	main_arena->attached_threads = cmain_arena->attached_threads;
	for (i = 0; i < NFASTBINS; i++) {
		main_arena->fastbinsY[i] = cmain_arena->fastbinsY[i];
	}
	main_arena->top = cmain_arena->top;
	main_arena->last_remainder = cmain_arena->last_remainder;
	for (i = 0; i < NBINS * 2 - 2; i++) {
		main_arena->bins[i] = cmain_arena->bins[i];
	}
	main_arena->next = cmain_arena->next;
	main_arena->next_free = cmain_arena->next_free;
	main_arena->system_mem = cmain_arena->system_mem;
	main_arena->max_system_mem = cmain_arena->max_system_mem;
}

static void GH(update_arena_without_tc)(GH(RzHeap_MallocState) * cmain_arena, MallocState *main_arena) {
	int i = 0;
	main_arena->mutex = cmain_arena->mutex;
	main_arena->flags = cmain_arena->flags;
	for (i = 0; i < BINMAPSIZE; i++) {
		main_arena->binmap[i] = cmain_arena->binmap[i];
	}
	main_arena->attached_threads = 1;
	for (i = 0; i < NFASTBINS; i++) {
		main_arena->fastbinsY[i] = cmain_arena->fastbinsY[i];
	}
	main_arena->top = cmain_arena->top;
	main_arena->last_remainder = cmain_arena->last_remainder;
	for (i = 0; i < NBINS * 2 - 2; i++) {
		main_arena->bins[i] = cmain_arena->bins[i];
	}
	main_arena->next = cmain_arena->next;
	main_arena->next_free = cmain_arena->next_free;
	main_arena->system_mem = cmain_arena->system_mem;
	main_arena->max_system_mem = cmain_arena->max_system_mem;
}

/**
 * \brief Store the MallocState struct of an arena with base address m_arena in main_arena
 * \param core RzCore pointer
 * \param m_arena The base address of malloc state struct of the arena
 * \param main_arena The MallocState struct in which the data is stored
 * \return True if the main_arena struct was successfully updated else False
 */
RZ_API bool GH(rz_heap_update_main_arena)(RzCore *core, GHT m_arena, MallocState *main_arena) {
	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	if (tcache) {
		GH(RzHeap_MallocState_tcache) *cmain_arena = RZ_NEW0(GH(RzHeap_MallocState_tcache));
		if (!cmain_arena) {
			return false;
		}
		(void)rz_io_read_at(core->io, m_arena, (ut8 *)cmain_arena, sizeof(GH(RzHeap_MallocState_tcache)));
		/* arena->next should point to itself even if there is only one thread */
		if (!cmain_arena->next) {
			return false;
		}
		GH(update_arena_with_tc)
		(cmain_arena, main_arena);
		free(cmain_arena);
	} else {
		GH(RzHeap_MallocState) *cmain_arena = RZ_NEW0(GH(RzHeap_MallocState));
		if (!cmain_arena) {
			return false;
		}
		(void)rz_io_read_at(core->io, m_arena, (ut8 *)cmain_arena, sizeof(GH(RzHeap_MallocState)));
		GH(update_arena_without_tc)
		(cmain_arena, main_arena);
		free(cmain_arena);
	}
	return true;
}

static void GH(get_brks)(RzCore *core, GHT *brk_start, GHT *brk_end) {
	if (rz_config_get_b(core->config, "cfg.debug")) {
		RzListIter *iter;
		RzDebugMap *map;
		rz_debug_map_sync(core->dbg);
		rz_list_foreach (core->dbg->maps, iter, map) {
			if (map->name) {
				if (strstr(map->name, "[heap]")) {
					*brk_start = map->addr;
					*brk_end = map->addr_end;
					break;
				}
			}
		}
	} else {
		void **it;
		RzPVector *maps = rz_io_maps(core->io);
		rz_pvector_foreach (maps, it) {
			RzIOMap *map = *it;
			if (map->name) {
				if (strstr(map->name, "[heap]")) {
					*brk_start = map->itv.addr;
					*brk_end = map->itv.addr + map->itv.size;
					break;
				}
			}
		}
	}
}

static void GH(print_arena_stats)(RzCore *core, GHT m_arena, MallocState *main_arena, GHT global_max_fast, int format) {
	size_t i, j, k, start;
	GHT align = 12 * SZ + sizeof(int) * 2;
	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (tcache) {
		align = 16;
	}

	GHT apart[NSMALLBINS + 1] = { 0LL };
	if (format == RZ_OUTPUT_MODE_RIZIN) {
		for (i = 0; i < NBINS * 2 - 2; i += 2) {
			GHT addr = m_arena + align + SZ * i - SZ * 2;
			GHT bina = main_arena->bins[i];
			rz_cons_printf("f chunk.%zu.bin @ 0x%" PFMT64x "\n", i, (ut64)addr);
			rz_cons_printf("f chunk.%zu.fd @ 0x%" PFMT64x "\n", i, (ut64)bina);
			bina = main_arena->bins[i + 1];
			rz_cons_printf("f chunk.%zu.bk @ 0x%" PFMT64x "\n", i, (ut64)bina);
		}
		for (i = 0; i < BINMAPSIZE; i++) {
			rz_cons_printf("f binmap.%zu @ 0x%" PFMT64x, i, (ut64)main_arena->binmap[i]);
		}
		{ /* maybe use SDB instead of flags for this? */
			char units[8];
			rz_num_units(units, sizeof(units), main_arena->max_system_mem);
			rz_cons_printf("f heap.maxmem @ %s\n", units);

			rz_num_units(units, sizeof(units), main_arena->system_mem);
			rz_cons_printf("f heap.sysmem @ %s\n", units);

			rz_num_units(units, sizeof(units), main_arena->next_free);
			rz_cons_printf("f heap.nextfree @ %s\n", units);

			rz_num_units(units, sizeof(units), main_arena->next);
			rz_cons_printf("f heap.next @ %s\n", units);
		}
		return;
	}

	PRINT_GA("malloc_state @ ");
	PRINTF_BA("0x%" PFMT64x "\n\n", (ut64)m_arena);
	PRINT_GA("struct malloc_state main_arena {\n");
	PRINT_GA("  mutex = ");
	PRINTF_BA("0x%08x\n", (ut32)main_arena->mutex);
	PRINT_GA("  flags = ");
	PRINTF_BA("0x%08x\n", (ut32)main_arena->flags);
	PRINT_GA("  fastbinsY = {\n");

	for (i = 0, j = 1, k = SZ * 4; i < NFASTBINS; i++, j++, k += SZ * 2) {
		if (FASTBIN_IDX_TO_SIZE(j) <= global_max_fast) {
			PRINTF_YA(" Fastbin %02zu\n", j);
		} else {
			PRINTF_RA(" Fastbin %02zu\n", j);
		}
		PRINT_GA(" chunksize:");
		PRINTF_BA(" == %04zu ", k);
		PRINTF_GA("0x%" PFMT64x, (ut64)main_arena->fastbinsY[i]);
		PRINT_GA(",\n");
	}
	PRINT_GA("}\n");
	PRINT_GA("  top = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->top);
	PRINT_GA(",\n");
	PRINT_GA("  last_remainder = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->last_remainder);
	PRINT_GA(",\n");
	PRINT_GA("  bins {\n");

	/* Index & size for largebins */
	start = SZ * 128;
	for (i = start, k = 0, j = 0; j < NBINS - 2 && i < 1024 * 1024; i += 64) {
		j = largebin_index(i);
		if (j == k + NSMALLBINS + 1) {
			apart[k++] = i;
		}
	}
	for (i = 0, j = 1, k = SZ * 4; i < NBINS * 2 - 2; i += 2, j++) {
		PRINTF_YA(" Bin %03zu: ", j);
		if (j == 1) {
			PRINT_GA("Unsorted Bin");
			PRINT_GA(" [");
			PRINT_GA(" chunksize:");
			PRINT_BA(" undefined ");
		} else if (j > 1 && j <= NSMALLBINS) {
			if (j == 2) {
				PRINT_GA("             ┌");
			} else if (j == (NSMALLBINS / 2)) {
				PRINT_GA("  Small Bins │");
			} else if (j != 2 && j != (NSMALLBINS / 2) && j != NSMALLBINS) {
				PRINT_GA("             │");
			} else {
				PRINT_GA("             └");
			}
			PRINT_GA(" chunksize:");
			PRINTF_BA(" == %06zu  ", k);
			if (j < NSMALLBINS) {
				k += SZ * 2;
			}
		} else {
			if (j == NSMALLBINS + 1) {
				PRINT_GA("             ┌");
			} else if (j == (NSMALLBINS / 2) * 3) {
				PRINT_GA("  Large Bins │");
			} else if (j != NSMALLBINS + 1 && j != (NSMALLBINS / 2) * 3 && j != NBINS - 1) {
				PRINT_GA("             │");
			} else {
				PRINT_GA("             └");
			}
			PRINT_GA(" chunksize:");
			if (j != NBINS - 1) {
				PRINTF_BA(" >= %06" PFMT64d "  ", (ut64)apart[j - NSMALLBINS - 1]);
			} else {
				PRINT_BA(" remaining ");
			}
		}
		GHT bin = m_arena + align + SZ * i - SZ * 2;
		PRINTF_GA("0x%" PFMT64x "->fd = ", (ut64)bin);
		PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->bins[i]);
		PRINT_GA(", ");
		PRINTF_GA("0x%" PFMT64x "->bk = ", (ut64)bin);
		PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->bins[i + 1]);
		PRINT_GA(", ");
		rz_cons_newline();
	}

	PRINT_GA("  }\n");
	PRINT_GA("  binmap = {");

	for (i = 0; i < BINMAPSIZE; i++) {
		if (i) {
			PRINT_GA(",");
		}
		PRINTF_BA("0x%x", (ut32)main_arena->binmap[i]);
	}
	PRINT_GA("}\n");
	PRINT_GA("  next = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->next);
	PRINT_GA(",\n");
	PRINT_GA("  next_free = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->next_free);
	PRINT_GA(",\n");
	PRINT_GA("  system_mem = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->system_mem);
	PRINT_GA(",\n");
	PRINT_GA("  max_system_mem = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)main_arena->max_system_mem);
	PRINT_GA(",\n");
	PRINT_GA("}\n\n");
}

static inline bool GH(is_map_name_libc)(const char *map_name) {
	return strstr(map_name, "/libc-") || strstr(map_name, "/libc.");
}

/**
 * \brief Store the base address of main arena at m_arena
 * \param core RzCore pointer
 * \param m_arena Store the location of main arena at this integer pointer
 * \return True if a main arena was found else False
 */
RZ_API bool GH(rz_heap_resolve_main_arena)(RzCore *core, GHT *m_arena) {
	rz_return_val_if_fail(core && core->dbg && core->dbg->maps, false);

	GHT brk_start = GHT_MAX, brk_end = GHT_MAX;
	GHT libc_addr_sta = GHT_MAX, libc_addr_end = 0;
	GHT addr_srch = GHT_MAX, heap_sz = GHT_MAX;
	GHT main_arena_sym = GHT_MAX;
	bool is_debugged = rz_config_get_b(core->config, "cfg.debug");
	bool first_libc = true;
	rz_config_set_i(core->config, "dbg.glibc.tcache", GH(is_tcache)(core));

	if (is_debugged) {
		RzListIter *iter;
		RzDebugMap *map;
		rz_debug_map_sync(core->dbg);
		rz_list_foreach (core->dbg->maps, iter, map) {
			/* Try to find the main arena address using the glibc's symbols. */
			if (GH(is_map_name_libc)(map->name) && first_libc && main_arena_sym == GHT_MAX) {
				first_libc = false;
				main_arena_sym = GH(get_main_arena_with_symbol)(core, map);
			}
			if (GH(is_map_name_libc)(map->name) && map->perm == RZ_PERM_RW) {
				libc_addr_sta = map->addr;
				libc_addr_end = map->addr_end;
				break;
			}
		}
	} else {
		void **it;
		RzPVector *maps = rz_io_maps(core->io);
		rz_pvector_foreach (maps, it) {
			RzIOMap *map = *it;
			if (map->name && strstr(map->name, "arena")) {
				libc_addr_sta = map->itv.addr;
				libc_addr_end = map->itv.addr + map->itv.size;
				break;
			}
		}
	}

	if (libc_addr_sta == GHT_MAX || libc_addr_end == GHT_MAX) {
		if (rz_config_get_b(core->config, "cfg.debug")) {
			RZ_LOG_WARN("core: Can't find glibc mapped in memory (see dm)\n");
		} else {
			RZ_LOG_WARN("core: Can't find arena mapped in memory (see om)\n");
		}
		return false;
	}

	GH(get_brks)
	(core, &brk_start, &brk_end);
	if (brk_start == GHT_MAX || brk_end == GHT_MAX) {
		RZ_LOG_ERROR("core: no heap section\n");
		return false;
	}

	addr_srch = libc_addr_sta;
	heap_sz = brk_end - brk_start;
	MallocState *ta = RZ_NEW0(MallocState);
	if (!ta) {
		return false;
	}

	if (main_arena_sym != GHT_MAX) {
		GH(rz_heap_update_main_arena)
		(core, main_arena_sym, ta);
		*m_arena = main_arena_sym;
		core->dbg->main_arena_resolved = true;
		free(ta);
		return true;
	}
	while (addr_srch < libc_addr_end) {
		GH(rz_heap_update_main_arena)
		(core, addr_srch, ta);
		if (ta->top > brk_start && ta->top < brk_end &&
			ta->system_mem == heap_sz) {

			*m_arena = addr_srch;
			free(ta);
			if (is_debugged) {
				core->dbg->main_arena_resolved = true;
			}
			return true;
		}
		addr_srch += sizeof(GHT);
	}
	RZ_LOG_WARN("core: Can't find main_arena in mapped memory\n");
	free(ta);
	return false;
}

/**
 * \brief Parses tcache information from the given address in the target process memory.
 * \param core RzCore Pointer to the Rizin's core
 * \param tls_addr Address of the thread-local storage base address.
 * \param tid Thread ID.
 * \return True if tcache information was successfully parsed else false.
 *
 * Parse the tcache and tcache bins struct for the provided thread local base and print it.
 */

static bool GH(parse_tcache_from_addr)(RzCore *core, const GHT tls_addr, const GHT tid) {
	RzDebugMap *map;
	RzListIter *iter;
	ut8 tcache_addr[8] = { 0 };
	GH(RTcache) *tcache_heap = NULL;

	rz_list_foreach (core->dbg->maps, iter, map) {
		/*
		 * TODO: Send a list of maps with this page size and perms
		 * instead of traversing through every map
		 */
		if (map->size != HEAP_PAGE_SIZE || map->perm != RZ_PERM_RW) {
			if (strcmp(core->dbg->arch, "x86") || map->size != HEAP_PAGE_SIZE_X86) {
				continue;
			}
		}

		rz_io_nread_at(core->io, tls_addr, tcache_addr, sizeof(GHT));
		const GHT tcache_guess = GH(read_val)(core, tcache_addr, false);
		if (tcache_guess < map->addr || tcache_guess > map->addr_end) {
			continue;
		}

#if __aarch64__
		/* We will encounter main_arena pointer somewhere in ARM64 */
		if (GH(is_arena)(core, tcache_guess, GHT_MIN)) {
			break;
		}
#endif
		tcache_heap = GH(tcache_new)(core);
		if (!GH(tcache_read)(core, tcache_guess, tcache_heap)) {
			GH(tcache_free)
			(tcache_heap);
			tcache_heap = NULL;
		}
		break;
	}
	if (tcache_heap != NULL) {
		RzList *bins = GH(fill_tcache_entries)(core, tcache_heap);
		GH(print_tcache)
		(core, bins, NULL, tid);
		GH(tcache_free)
		(tcache_heap);
		tcache_heap = NULL;
		return true;
	}

	return false;
}

/**
 * \brief Parses Thread-Local Storage (TLS) data for a given thread ID to identify tcache structures.
 * \param core RzCore Pointer to the Rizin's core
 * \param th RzDebugPid Pointer to the Rizin's Debug PID structure representing the thread.
 * \param tid Thread ID.
 * \return True if tcache and bin structures were successfully parsed from TLS data else False.
 *
 * Parse the TLS data and identify the tcache and tcache-bins for the given thread ID.
 */

static bool GH(parse_tls_data)(RzCore *core, RZ_NONNULL RzDebugPid *th, GHT tid) {
	rz_return_val_if_fail(th, false);
	GHT tls_addr = GHT_MAX;
	GHT addr = GHT_MAX;

	if (!th->tls) {
		return false;
	}

#if __x86_64__
	ut8 dtv[sizeof(GHT)] = { 0 };
	rz_io_nread_at(core->io, th->tls + SZ, dtv, sizeof(GHT));
	addr = GH(read_val)(core, dtv, false);
	memset(dtv, 0, sizeof(dtv));
	/*
	 * https://github.com/jart/cosmopolitan/blob/06839ab3017d86e87db3ec740a2b5e00d9fe9e11/libc/runtime/enable_tls.c#L65
	 */
	// size of dtv is SZ*2
	rz_io_nread_at(core->io, addr + SZ * 2, dtv, sizeof(GHT));
	addr = GH(read_val)(core, dtv, false);
#elif __aarch64__
	/*
	 * https://github.com/jart/cosmopolitan/blob/06839ab3017d86e87db3ec740a2b5e00d9fe9e11/libc/runtime/enable_tls.c#L79
	 */
	addr = th->tls + SZ * 2;
#endif
	const GHT end = addr + 0x10 * SZ * 2;
	// Parse tls data and check if it complies with tcache struct
	for (tls_addr = addr; tls_addr <= end; tls_addr += SZ) {
		if (GH(parse_tcache_from_addr)(core, tls_addr, tid)) {
			return true;
		}
	}
	return false;
}

/**
 * \brief Resolves tcache structures per thread.
 * \param core RzCore Pointer to the Rizin's core
 *
 * Resolves the TLS base for every thread and parse to identify the tcache structures.
 */

static void GH(resolve_tcache_perthread)(RZ_NONNULL RzCore *core) {
	RzDebugPid *th;
	RzListIter *it;
	GHT tid = 1;
	RzDebug *dbg = core->dbg;

	rz_list_foreach (dbg->threads, it, th) {
		// First try: fetch tls value and update debug pid
		if (!th->tls) {
			th->tls = rz_debug_get_tls(core->dbg, th->pid);
		}
		if (!GH(parse_tls_data)(core, th, tid++)) {
			// Second try: Update the thread list if the tls parsing fails.
			RzList *thread_list = rz_debug_native_threads(dbg, dbg->pid);
			RzDebugPid *thread_dbg = rz_debug_get_thread(thread_list, th->pid);
			if (thread_dbg) {
				GH(parse_tls_data)
				(core, thread_dbg, tid);
			}
		}
	}
}

RZ_API RZ_OWN bool GH(resolve_heap_tcache)(RZ_NONNULL RzCore *core, GHT arena_base) {
	RzDebug *dbg = core->dbg;

	if (dbg->threads) {
		GH(resolve_tcache_perthread)
		(core);
		return true;
	}

	// Only main thread is present
	RzList *bins = GH(rz_heap_tcache_content)(core, arena_base);
	GH(print_tcache)
	(core, bins, NULL, 0);

	return true;
}

void GH(print_heap_chunk)(RzCore *core, GHT chunk) {
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (!cnk) {
		return;
	}

	(void)rz_io_read_at(core->io, chunk, (ut8 *)cnk, sizeof(*cnk));

	PRINT_GA("struct malloc_chunk @ ");
	PRINTF_BA("0x%" PFMT64x, (ut64)chunk);
	PRINT_GA(" {\n  prev_size = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)cnk->prev_size);
	PRINT_GA(",\n  size = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)cnk->size & ~(NON_MAIN_ARENA | IS_MMAPPED | PREV_INUSE));
	PRINT_GA(",\n  flags: |N:");
	PRINTF_BA("%1" PFMT64u, (ut64)(cnk->size & NON_MAIN_ARENA) >> 2);
	PRINT_GA(" |M:");
	PRINTF_BA("%1" PFMT64u, (ut64)(cnk->size & IS_MMAPPED) >> 1);
	PRINT_GA(" |P:");
	PRINTF_BA("%1" PFMT64u, (ut64)cnk->size & PREV_INUSE);

	PRINT_GA(",\n  fd = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)cnk->fd);

	PRINT_GA(",\n  bk = ");
	PRINTF_BA("0x%" PFMT64x, (ut64)cnk->bk);

	if (cnk->size > SZ * 128) {
		PRINT_GA(",\n  fd-nextsize = ");
		PRINTF_BA("0x%" PFMT64x, (ut64)cnk->fd_nextsize);
		PRINT_GA(",\n  bk-nextsize = ");
		PRINTF_BA("0x%" PFMT64x, (ut64)cnk->bk_nextsize);
	}

	PRINT_GA(",\n}\n");
	GHT size = ((cnk->size >> 3) << 3) - SZ * 2;
	if (size > SZ * 128) {
		PRINT_GA("chunk too big to be displayed\n");
		size = SZ * 128;
	}

	char *data = calloc(1, size);
	if (data) {
		rz_io_read_at(core->io, chunk + SZ * 2, (ut8 *)data, size);
		PRINT_GA("chunk data = \n");
		rz_core_print_hexdump(core, chunk + SZ * 2, (ut8 *)data, size, SZ * 8, SZ, 1);
		free(data);
	}
	free(cnk);
}

/**
 * \brief Get a heap chunk with base address <addr>
 * \param core RzCore pointer
 * \param addr Base address of the chunk
 * \return RzHeapChunk struct pointer of the chunk
 */
RZ_API GH(RzHeapChunk) * GH(rz_heap_get_chunk_at_addr)(RzCore *core, GHT addr) {
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk) {
		return NULL;
	}
	(void)rz_io_nread_at(core->io, addr, (ut8 *)cnk, sizeof(*cnk));
	return cnk;
}

/**
 * \brief Prints compact representation of a heap chunk. Format: Chunk(addr=, size=, flags=)
 * \param core RzCore pointer
 * \param chunk Offset of the chunk in memory
 */
void GH(print_heap_chunk_simple)(RzCore *core, GHT chunk, const char *status, PJ *pj) {
	GH(RzHeapChunk) *cnk = GH(rz_heap_get_chunk_at_addr)(core, chunk);
	if (!cnk) {
		return;
	}
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (pj == NULL) {
		PRINT_GA("Chunk");
		rz_cons_printf("(");
		if (status) {
			rz_cons_printf("status=");
			if (!strcmp(status, "free")) {
				PRINTF_GA("%s", status);
				rz_cons_printf("%-6s", ",");
			} else {
				rz_cons_printf("%s,", status);
			}
			rz_cons_printf(" ");
		}
		rz_cons_printf("addr=");
		PRINTF_YA("0x%" PFMT64x, (ut64)chunk);
		rz_cons_printf(", size=");
		PRINTF_BA("0x%" PFMT64x, (ut64)cnk->size & ~(NON_MAIN_ARENA | IS_MMAPPED | PREV_INUSE));
		rz_cons_printf(", flags=");
		bool print_comma = false;
		if (cnk->size & NON_MAIN_ARENA) {
			PRINT_RA("NON_MAIN_ARENA");
			print_comma = true;
		}
		if (cnk->size & IS_MMAPPED) {
			if (print_comma) {
				PRINT_RA(",");
			}
			PRINT_RA("IS_MMAPPED");
			print_comma = true;
		}
		if (cnk->size & PREV_INUSE) {
			if (print_comma) {
				PRINT_RA(",");
			}
			PRINT_RA("PREV_INUSE");
		}
		rz_cons_printf(")");
	} else {
		pj_o(pj);
		pj_kn(pj, "prev_size", cnk->prev_size);
		pj_kn(pj, "addr", chunk);
		pj_kn(pj, "size", (ut64)cnk->size & ~(NON_MAIN_ARENA | IS_MMAPPED | PREV_INUSE));
		pj_kn(pj, "non_main_arena", cnk->size & NON_MAIN_ARENA);
		pj_kn(pj, "mmapped", cnk->size & IS_MMAPPED);
		pj_kn(pj, "prev_inuse", cnk->size & PREV_INUSE);
		pj_kn(pj, "fd", cnk->fd);
		pj_kn(pj, "bk", cnk->bk);
		pj_end(pj);
	}
	free(cnk);
}

static bool GH(is_arena)(RzCore *core, GHT m_arena, GHT m_state) {
	if (m_arena == m_state) {
		return true;
	}
	MallocState *ta = RZ_NEW0(MallocState);
	if (!ta) {
		return false;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_arena, ta)) {
		free(ta);
		return false;
	}
	if (ta->next == m_state) {
		free(ta);
		return true;
	}
	while (ta->next != GHT_MAX && ta->next != m_arena) {
		if (!GH(rz_heap_update_main_arena)(core, ta->next, ta)) {
			free(ta);
			return false;
		}
		if (ta->next == m_state) {
			free(ta);
			return true;
		}
	}
	free(ta);
	if (m_state == GHT_MIN) {
		return true;
	}
	return false;
}

static int GH(print_double_linked_list_bin_simple)(RzCore *core, GHT bin, MallocState *main_arena, GHT brk_start) {
	GHT next = GHT_MAX;
	int ret = 1;
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (!cnk) {
		return -1;
	}

	rz_io_read_at(core->io, bin, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));

	PRINTF_GA("    0x%" PFMT64x, (ut64)bin);
	if (cnk->fd != bin) {
		ret = 0;
	}
	while (cnk->fd != bin) {
		PRINTF_BA("->fd = 0x%" PFMT64x, (ut64)cnk->fd);
		next = cnk->fd;
		if (next < brk_start || next > main_arena->top) {
			PRINT_RA("Double linked list corrupted\n");
			free(cnk);
			return -1;
		}
		rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	}

	PRINTF_GA("->fd = 0x%" PFMT64x, (ut64)cnk->fd);
	next = cnk->fd;

	if (next != bin) {
		PRINT_RA("Double linked list corrupted\n");
		free(cnk);
		return -1;
	}
	(void)rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	PRINTF_GA("\n    0x%" PFMT64x, (ut64)bin);

	while (cnk->bk != bin) {
		PRINTF_BA("->bk = 0x%" PFMT64x, (ut64)cnk->bk);
		next = cnk->bk;
		if (next < brk_start || next > main_arena->top) {
			PRINT_RA("Double linked list corrupted.\n");
			free(cnk);
			return -1;
		}
		(void)rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	}

	PRINTF_GA("->bk = 0x%" PFMT64x, (ut64)cnk->bk);
	free(cnk);
	return ret;
}

static int GH(print_double_linked_list_bin_graph)(RzCore *core, GHT bin, MallocState *main_arena, GHT brk_start) {
	RzAGraph *g = rz_agraph_new(rz_cons_canvas_new(1, 1));
	GHT next = GHT_MAX;
	char title[256], chunk[256];
	RzANode *bin_node = NULL, *prev_node = NULL, *next_node = NULL;
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (!cnk || !g) {
		free(cnk);
		rz_agraph_free(g);
		return -1;
	}
	g->can->color = rz_config_get_i(core->config, "scr.color");

	(void)rz_io_read_at(core->io, bin, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	snprintf(title, sizeof(title) - 1, "bin @ 0x%" PFMT64x "\n", (ut64)bin);
	snprintf(chunk, sizeof(chunk) - 1, "fd: 0x%" PFMT64x "\nbk: 0x%" PFMT64x "\n",
		(ut64)cnk->fd, (ut64)cnk->bk);
	bin_node = rz_agraph_add_node(g, title, chunk);
	prev_node = bin_node;

	while (cnk->bk != bin) {
		next = cnk->bk;
		if (next < brk_start || next > main_arena->top) {
			PRINT_RA("Double linked list corrupted\n");
			free(cnk);
			free(g);
			return -1;
		}

		rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
		snprintf(title, sizeof(title) - 1, "Chunk @ 0x%" PFMT64x "\n", (ut64)next);
		snprintf(chunk, sizeof(chunk) - 1, "fd: 0x%" PFMT64x "\nbk: 0x%" PFMT64x "\n",
			(ut64)cnk->fd, (ut64)cnk->bk);
		next_node = rz_agraph_add_node(g, title, chunk);
		rz_agraph_add_edge(g, prev_node, next_node);
		rz_agraph_add_edge(g, next_node, prev_node);
		prev_node = next_node;
	}

	rz_agraph_add_edge(g, prev_node, bin_node);
	rz_agraph_add_edge(g, bin_node, prev_node);
	rz_agraph_print(g);

	free(cnk);
	rz_agraph_free(g);
	return 0;
}

static int GH(print_double_linked_list_bin)(RzCore *core, MallocState *main_arena, GHT m_arena, GHT offset, GHT num_bin, int graph) {
	if (!core || !core->dbg || !core->dbg->maps) {
		return -1;
	}
	int ret = 0;
	GHT brk_start = GHT_MAX, brk_end = GHT_MAX, initial_brk = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (num_bin > 126) {
		return -1;
	}
	GHT bin = main_arena->bins[num_bin];

	if (!bin) {
		return -1;
	}

	GH(get_brks)
	(core, &brk_start, &brk_end);
	if (brk_start == GHT_MAX || brk_end == GHT_MAX) {
		RZ_LOG_ERROR("core: no heap section\n");
		return -1;
	}

	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	if (tcache) {
		const int fc_offset = rz_config_get_i(core->config, "dbg.glibc.fc_offset");
		bin = m_arena + offset + SZ * num_bin * 2 + 10 * SZ;
		initial_brk = ((brk_start >> 12) << 12) + fc_offset;
	} else {
		bin = m_arena + offset + SZ * num_bin * 2 - SZ * 2;
		initial_brk = (brk_start >> 12) << 12;
	}

	if (num_bin == 0) {
		PRINT_GA("  double linked list unsorted bin {\n");
	} else if (num_bin >= 1 && num_bin <= NSMALLBINS - 1) {
		PRINT_GA("  double linked list small bin {\n");
	} else if (num_bin >= NSMALLBINS && num_bin <= NBINS - 2) {
		PRINT_GA("  double linked list large bin {\n");
	}

	if (!graph || graph == 1) {
		ret = GH(print_double_linked_list_bin_simple)(core, bin, main_arena, initial_brk);
	} else {
		ret = GH(print_double_linked_list_bin_graph)(core, bin, main_arena, initial_brk);
	}
	PRINT_GA("\n  }\n");
	return ret;
}

static void GH(print_heap_bin)(RzCore *core, GHT m_arena, MallocState *main_arena, const char *input, bool print_graph) {
	GHT num_bin = GHT_MAX;
	GHT offset;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	if (tcache) {
		offset = 16;
	} else {
		offset = 12 * SZ + sizeof(int) * 2;
	}

	if (!input[0]) {
		PRINT_YA("Bins {\n");
		for (size_t i = 0; i < NBINS - 1; i++) {
			PRINTF_YA(" Bin %03" PFMTSZu ":\n", i);
			GH(print_double_linked_list_bin)
			(core, main_arena, m_arena, offset, i, print_graph);
		}
		PRINT_YA("\n}\n");
	} else {
		num_bin = rz_num_get(NULL, input);
		if (num_bin > NBINS - 2) {
			RZ_LOG_ERROR("The number of bins is greater than %d\n", NBINS - 2);
			return;
		}
		PRINTF_YA("  Bin %03" PFMT64u ":\n", (ut64)num_bin);
		GH(print_double_linked_list_bin)
		(core, main_arena, m_arena, offset, num_bin, print_graph);
	}
	return;
}

void GH(rz_heap_chunk_free)(RzHeapChunkListItem *item) {
	if (!item) {
		return;
	}
	free(item->status);
	free(item);
}

RZ_API RzHeapBin *GH(rz_heap_fastbin_content)(RzCore *core, MallocState *main_arena, int bin_num) {
	if (!core || !core->dbg || !core->dbg->maps) {
		return NULL;
	}
	GHT brk_start = GHT_MAX, brk_end = GHT_MAX;
	RzHeapBin *heap_bin = RZ_NEW0(RzHeapBin);
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk || !heap_bin) {
		free(heap_bin);
		free(cnk);
		return NULL;
	}
	heap_bin->chunks = rz_list_newf((RzListFree)GH(rz_heap_chunk_free));
	heap_bin->bin_num = bin_num + 1;
	heap_bin->size = FASTBIN_IDX_TO_SIZE(bin_num + 1);
	heap_bin->type = rz_str_dup("Fast");
	GHT next = main_arena->fastbinsY[bin_num];
	if (!next) {
		free(cnk);
		return heap_bin;
	}
	GH(get_brks)
	(core, &brk_start, &brk_end);
	heap_bin->fd = next;
	if (brk_start == GHT_MAX || brk_end == GHT_MAX) {
		free(cnk);
		return heap_bin;
	}
	GHT size = main_arena->top - brk_start;

	GHT next_root = next, next_tmp = next, double_free = GHT_MAX;
	while (next && next >= brk_start && next < main_arena->top) {
		RzHeapChunkListItem *item = RZ_NEW0(RzHeapChunkListItem);
		if (!item) {
			break;
		}
		item->addr = next;
		item->status = rz_str_dup("free");
		rz_list_append(heap_bin->chunks, item);
		while (double_free == GHT_MAX && next_tmp && next_tmp >= brk_start && next_tmp <= main_arena->top) {
			rz_io_read_at(core->io, next_tmp, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
			next_tmp = GH(get_next_pointer)(core, next_tmp, cnk->fd);
			if (cnk->prev_size > size || ((cnk->size >> 3) << 3) > size) {
				break;
			}
			if (next_root == next_tmp) {
				double_free = next_root;
				break;
			}
		}
		rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
		next = GH(get_next_pointer)(core, next, cnk->fd);
		if (cnk->prev_size > size || ((cnk->size >> 3) << 3) > size) {
			char message[50];
			rz_snprintf(message, 50, "Linked list corrupted @ 0x%" PFMT64x, (ut64)next);
			heap_bin->message = rz_str_dup(message);
			free(cnk);
			return heap_bin;
		}

		next_root = next_tmp = next;
		if (double_free == next) {
			char message[50];
			rz_snprintf(message, 50, "Double free detected @ 0x%" PFMT64x, (ut64)next);
			heap_bin->message = rz_str_dup(message);
			free(cnk);
			return heap_bin;
		}
	}
	if (next && (next < brk_start || next >= main_arena->top)) {
		char message[50];
		rz_snprintf(message, 50, "Linked list corrupted @ 0x%" PFMT64x, (ut64)next);
		heap_bin->message = rz_str_dup(message);
		free(cnk);
		return heap_bin;
	}
	free(cnk);
	return heap_bin;
}

void GH(print_heap_fastbin)(RzCore *core, GHT m_arena, MallocState *main_arena, GHT global_max_fast, const char *input, bool main_arena_only, PJ *pj) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	int fastbins_max = rz_config_get_i(core->config, "dbg.glibc.fastbinmax") - 1;
	int global_max_fast_idx = fastbin_index(global_max_fast);
	int fastbin_count = fastbins_max < global_max_fast_idx ? fastbins_max : global_max_fast_idx;
	int bin_to_print = 0;
	if (input[0]) {
		bin_to_print = (int)rz_num_get(NULL, input);
		if (bin_to_print <= 0 || bin_to_print - 1 > fastbin_count) {
			RZ_LOG_ERROR("core: the number of bins is greater than %d\n", fastbin_count + 1);
			return;
		}
	}
	if (!pj) {
		rz_cons_printf("Fast bins in Arena @ ");
		PRINTF_YA("0x%" PFMT64x, (ut64)m_arena);
		rz_cons_newline();
	}
	for (int i = 0; i <= fastbin_count; i++) {
		if (bin_to_print && i != bin_to_print - 1) {
			continue;
		}
		RzHeapBin *bin = GH(rz_heap_fastbin_content)(core, main_arena, i);
		if (!bin) {
			continue;
		}
		if (!pj) {
			rz_cons_printf("Fast_bin[");
			PRINTF_BA("%02zu", (size_t)bin->bin_num);
			rz_cons_printf("] [size: ");
			PRINTF_BA("0x%" PFMT64x, bin->size);
			rz_cons_printf("]");
		} else {
			pj_o(pj);
			pj_ks(pj, "bin_type", "fast");
			pj_kn(pj, "bin_num", bin->bin_num);
			pj_ka(pj, "chunks");
		}
		if (!bin->chunks || !rz_list_length(bin->chunks)) {
			if (!pj) {
				PRINT_RA(" Empty bin\n");
			}
		} else {
			RzListIter *iter;
			RzHeapChunkListItem *pos;
			rz_cons_newline();
			rz_list_foreach (bin->chunks, iter, pos) {
				if (!pj) {
					rz_cons_printf(" -> ");
				}
				GH(print_heap_chunk_simple)
				(core, pos->addr, NULL, pj);
				if (!pj) {
					rz_cons_newline();
				}
			}
			if (bin->message && !pj) {
				PRINTF_RA("%s\n", bin->message);
			}
		}
		if (pj) {
			pj_end(pj);
			pj_end(pj);
		}
		GH(rz_heap_bin_free)
		(bin);
	}
}

static GH(RTcache) * GH(tcache_new)(RzCore *core) {
	rz_return_val_if_fail(core, NULL);
	GH(RTcache) *tcache = RZ_NEW0(GH(RTcache));
	if (!tcache) {
		return NULL;
	}
	if (core->dbg->glibc_version >= TCACHE_NEW_VERSION) {
		tcache->type = NEW;
		tcache->RzHeapTcache.heap_tcache = RZ_NEW0(GH(RzHeapTcache));
	} else {
		tcache->type = OLD;
		tcache->RzHeapTcache.heap_tcache_pre_230 = RZ_NEW0(GH(RzHeapTcachePre230));
	}
	return tcache;
}

RZ_API void GH(tcache_free)(GH(RTcache) * tcache) {
	rz_return_if_fail(tcache);
	tcache->type == NEW
		? free(tcache->RzHeapTcache.heap_tcache)
		: free(tcache->RzHeapTcache.heap_tcache_pre_230);
	free(tcache);
}

static bool GH(tcache_read)(RzCore *core, GHT tcache_start, GH(RTcache) * tcache) {
	rz_return_val_if_fail(core && tcache, false);
	return tcache->type == NEW
		? rz_io_read_at(core->io, tcache_start, (ut8 *)tcache->RzHeapTcache.heap_tcache, sizeof(GH(RzHeapTcache)))
		: rz_io_read_at(core->io, tcache_start, (ut8 *)tcache->RzHeapTcache.heap_tcache_pre_230, sizeof(GH(RzHeapTcachePre230)));
}

static int GH(tcache_get_count)(GH(RTcache) * tcache, int index) {
	rz_return_val_if_fail(tcache, 0);
	return tcache->type == NEW
		? tcache->RzHeapTcache.heap_tcache->counts[index]
		: tcache->RzHeapTcache.heap_tcache_pre_230->counts[index];
}

static GHT GH(tcache_get_entry)(GH(RTcache) * tcache, int index) {
	rz_return_val_if_fail(tcache, 0);
	return tcache->type == NEW
		? tcache->RzHeapTcache.heap_tcache->entries[index]
		: tcache->RzHeapTcache.heap_tcache_pre_230->entries[index];
}

/**
 * \brief Get a list of bins for the tcache associated with arena with base address arena_base
 * \param core RzCore pointer
 * \param arena_base Base address of the arena
 * \return RzList of RzHeapBin pointers
 */
RZ_API RzList /*<RzHeapBin *>*/ *GH(rz_heap_tcache_content)(RzCore *core, GHT arena_base) {
	// check if tcache is even present in this Glibc version
	const int tc = rz_config_get_i(core->config, "dbg.glibc.tcache");
	if (!tc) {
		rz_cons_printf("No tcache present in this version of libc\n");
		return NULL;
	}

	// get main arena base address to compare
	GHT m_arena;
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		return NULL;
	}

	GHT brk_start = GHT_MAX, brk_end = GHT_MAX, initial_brk = GHT_MAX;
	GH(get_brks)
	(core, &brk_start, &brk_end);
	GHT fc_offset = GH(tcache_chunk_size)(core, brk_start);
	initial_brk = brk_start + fc_offset;
	if (brk_start == GHT_MAX || brk_end == GHT_MAX || initial_brk == GHT_MAX) {
		// no heap section exists in this case
		return NULL;
	}

	// get the base address of tcache
	GHT tcache_start;
	if (arena_base == m_arena) {
		// get tcache base for main arena
		// tcache is consistently the first allocation in the main arena.
		tcache_start = brk_start + 0x10;
	} else {
		// get tcache base for thread arena
		GHT mmap_start = ((arena_base >> 16) << 16);
		tcache_start = mmap_start + sizeof(GH(RzHeapInfo)) + sizeof(GH(RzHeap_MallocState_tcache)) + GH(MMAP_ALIGN);

		// for thread arena check if the arena has threads attached or not
		MallocState *arena = RZ_NEW0(MallocState);
		if (!arena) {
			return NULL;
		}
		if (!GH(rz_heap_update_main_arena)(core, arena_base, arena) || !arena->attached_threads) {
			free(arena);
			return NULL;
		}
		free(arena);
	}
	// Get rz_tcache struct
	GH(RTcache) *tcache = GH(tcache_new)(core);
	if (!GH(tcache_read)(core, tcache_start, tcache)) {
		GH(tcache_free)
		(tcache);
		return NULL;
	}

	// List of heap bins to return
	RzList *tcache_bins_list = rz_list_newf((RzListFree)GH(rz_heap_bin_free));

	// Use rz_tcache struct to get bins
	for (int i = 0; i < TCACHE_MAX_BINS; i++) {
		int count = GH(tcache_get_count)(tcache, i);
		GHT entry = GH(tcache_get_entry)(tcache, i);

		RzHeapBin *bin = RZ_NEW0(RzHeapBin);
		if (!bin) {
			goto error;
		}
		bin->type = rz_str_dup("Tcache");
		bin->bin_num = i;
		bin->chunks = rz_list_newf((RzListFree)GH(rz_heap_chunk_free));
		if (!bin->chunks) {
			GH(rz_heap_bin_free)
			(bin);
			goto error;
		}

		rz_list_append(tcache_bins_list, bin);
		if (count <= 0) {
			continue;
		}
		bin->fd = (ut64)(entry - GH(HDR_SZ));
		// get first chunk
		RzHeapChunkListItem *chunk = RZ_NEW0(RzHeapChunkListItem);
		if (!chunk) {
			goto error;
		}
		chunk->addr = (ut64)(entry - GH(HDR_SZ));
		rz_list_append(bin->chunks, chunk);

		if (count <= 1) {
			continue;
		}

		// get rest of the chunks
		GHT tcache_fd = entry;
		GHT tcache_tmp = GHT_MAX;
		for (size_t n = 1; n < count; n++) {
			int r = rz_io_nread_at(core->io, tcache_fd, (ut8 *)&tcache_tmp, sizeof(GHT));
			if (r <= 0) {
				goto error;
			}
			tcache_tmp = GH(get_next_pointer)(core, tcache_fd, read_le(&tcache_tmp));
			chunk = RZ_NEW0(RzHeapChunkListItem);
			if (!chunk) {
				goto error;
			}
			// the base address of the chunk = address - 2 * PTR_SIZE
			chunk->addr = (ut64)(tcache_tmp - GH(HDR_SZ));
			rz_list_append(bin->chunks, chunk);
			tcache_fd = tcache_tmp;
		}
	}
	free(tcache);
	return tcache_bins_list;

error:
	rz_list_free(tcache_bins_list);
	free(tcache);
	return NULL;
}

static void GH(print_tcache_content)(RzCore *core, GHT arena_base, GHT main_arena_base, PJ *pj) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	RzList *bins = GH(rz_heap_tcache_content)(core, arena_base);
	if (!bins) {
		return;
	}
	if (!pj) {
		if (main_arena_base == arena_base) {
			rz_cons_printf("Tcache bins in Main Arena @ ");
		} else {
			rz_cons_printf("Tcache bins in Thread Arena @ ");
		}
		PRINTF_YA("0x%" PFMT64x "\n", (ut64)arena_base);
	}
	GH(print_tcache)
	(core, bins, pj, 0);
}

void GH(print_malloc_states)(RzCore *core, GHT m_arena, MallocState *main_arena, bool json) {
	MallocState *ta = RZ_NEW0(MallocState);
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	if (!ta) {
		return;
	}
	PJ *pj = NULL;
	if (!json) {
		rz_cons_printf("Main arena  (addr=");
		PRINTF_YA("0x%" PFMT64x, (ut64)m_arena);
		rz_cons_printf(", lastRemainder=");
		PRINTF_YA("0x%" PFMT64x, (ut64)main_arena->last_remainder);
		rz_cons_printf(", top=");
		PRINTF_YA("0x%" PFMT64x, (ut64)main_arena->top);
		rz_cons_printf(", next=");
		PRINTF_YA("0x%" PFMT64x, (ut64)main_arena->next);
		rz_cons_printf(")\n");
	} else {
		pj = pj_new();
		if (!pj) {
			free(ta);
			return;
		}
		pj_o(pj);
		pj_ka(pj, "arenas");
		pj_o(pj);
		pj_kn(pj, "addr", m_arena);
		pj_kn(pj, "last_rem", main_arena->last_remainder);
		pj_kn(pj, "top", main_arena->top);
		pj_kn(pj, "next", main_arena->next);
		pj_ks(pj, "type", "main");
		pj_ks(pj, "state", "used");
		pj_end(pj);
	}
	if (main_arena->next != m_arena) {
		ta->next = main_arena->next;
		while (GH(is_arena)(core, m_arena, ta->next) && ta->next != m_arena) {
			ut64 ta_addr = ta->next;
			/* If the pointer is equal to unsigned -1, we assume it is invalid */
			if (ta->next == GHT_MAX) {
				break;
			}
			if (!GH(rz_heap_update_main_arena)(core, ta->next, ta)) {
				goto end;
			}
			if (!json) {
				rz_cons_printf("Thread arena(addr=");
				PRINTF_YA("0x%" PFMT64x, ta_addr);
				rz_cons_printf(", lastRemainder=");
				PRINTF_YA("0x%" PFMT64x, (ut64)ta->last_remainder);
				rz_cons_printf(", top=");
				PRINTF_YA("0x%" PFMT64x, (ut64)ta->top);
				rz_cons_printf(", next=");
				PRINTF_YA("0x%" PFMT64x, (ut64)ta->next);
				if (ta->attached_threads) {
					rz_cons_printf(")\n");
				} else {
					rz_cons_printf(" free)\n");
				}
			} else {
				pj_o(pj);
				pj_kn(pj, "addr", (ut64)ta_addr);
				pj_kn(pj, "last_rem", ta->last_remainder);
				pj_kn(pj, "top", ta->top);
				pj_kn(pj, "next", ta->next);
				pj_ks(pj, "type", "thread");
				if (ta->attached_threads) {
					pj_ks(pj, "state", "used");
				} else {
					pj_ks(pj, "state", "free");
				}
				pj_end(pj);
			}
		}
	}
end:
	if (json) {
		pj_end(pj);
		pj_end(pj);
		rz_cons_println(pj_string(pj));
		pj_free(pj);
	}
	free(ta);
}

void GH(print_inst_minfo)(RzCore *core, GH(RzHeapInfo) * heap_info, GHT hinfo) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;

	PRINT_YA("malloc_info @ ");
	PRINTF_BA("0x%" PFMT64x, (ut64)hinfo);
	PRINT_YA(" {\n  ar_ptr = ");
	PRINTF_BA("0x%" PFMT64x "\n", (ut64)heap_info->ar_ptr);
	PRINT_YA("  prev = ");
	PRINTF_BA("0x%" PFMT64x "\n", (ut64)heap_info->prev);
	PRINT_YA("  size = ");
	PRINTF_BA("0x%" PFMT64x "\n", (ut64)heap_info->size);
	PRINT_YA("  mprotect_size = ");
	PRINTF_BA("0x%" PFMT64x "\n", (ut64)heap_info->mprotect_size);
	PRINT_YA("}\n\n");
}

void GH(print_malloc_info)(RzCore *core, GHT m_state, GHT malloc_state) {
	GHT h_info;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (malloc_state == m_state) {
		PRINT_RA("main_arena does not have an instance of malloc_info\n");
	} else if (GH(is_arena)(core, malloc_state, m_state)) {

		h_info = (malloc_state >> 16) << 16;
		GH(RzHeapInfo) *heap_info = RZ_NEW0(GH(RzHeapInfo));
		if (!heap_info) {
			return;
		}
		rz_io_read_at(core->io, h_info, (ut8 *)heap_info, sizeof(GH(RzHeapInfo)));
		GH(print_inst_minfo)
		(core, heap_info, h_info);
		MallocState *ms = RZ_NEW0(MallocState);
		if (!ms) {
			free(heap_info);
			return;
		}

		while (heap_info->prev != 0x0 && heap_info->prev != GHT_MAX) {
			if (!GH(rz_heap_update_main_arena)(core, malloc_state, ms)) {
				free(ms);
				free(heap_info);
				return;
			}
			if ((ms->top >> 16) << 16 != h_info) {
				h_info = (ms->top >> 16) << 16;
				rz_io_read_at(core->io, h_info, (ut8 *)heap_info, sizeof(GH(RzHeapInfo)));
				GH(print_inst_minfo)
				(core, heap_info, h_info);
			}
		}
		free(heap_info);
		free(ms);
	} else {
		PRINT_RA("This address is not part of the arenas\n");
	}
}

char *GH(rz_bin_num_to_type)(int bin_num) {
	if (bin_num == 0) {
		return rz_str_dup("Unsorted");
	} else if (bin_num >= 1 && bin_num <= NSMALLBINS - 1) {
		return rz_str_dup("Small");
	} else if (bin_num >= NSMALLBINS && bin_num <= NBINS - 2) {
		return rz_str_dup("Large");
	}
	return NULL;
}

RZ_API void GH(rz_heap_bin_free)(RzHeapBin *bin) {
	if (!bin) {
		return;
	}
	free(bin->type);
	free(bin->message);
	rz_list_free(bin->chunks);
	free(bin);
}
/**
 * \brief Get information about <bin_num> bin from NBINS array of an arena.
 * \param core RzCore pointer
 * \param main_arena MallocState struct of arena
 * \param bin_num bin number of bin whose chunk list you want
 * \return RzHeapBin struct for the bin
 */
RZ_API RzHeapBin *GH(rz_heap_bin_content)(RzCore *core, MallocState *main_arena, int bin_num, GHT m_arena) {
	int idx = 2 * bin_num;
	ut64 fw = main_arena->bins[idx];
	ut64 bk = main_arena->bins[idx + 1];
	RzHeapBin *bin = RZ_NEW0(RzHeapBin);
	if (!bin) {
		return NULL;
	}
	bin->fd = fw;
	bin->bk = bk;
	bin->bin_num = bin_num;
	bin->type = GH(rz_bin_num_to_type)(bin_num);

	// small bins hold chunks of a fixed size
	if (!strcmp(bin->type, "Small")) {
		bin->size = 4 * SZ + (bin_num - 1) * 2 * SZ;
	}

	bin->chunks = rz_list_newf(free);
	GH(RzHeapChunk) *head = RZ_NEW0(GH(RzHeapChunk));
	if (!head) {
		GH(rz_heap_bin_free)
		(bin);
		return NULL;
	}

	(void)rz_io_read_at(core->io, bk, (ut8 *)head, sizeof(GH(RzHeapChunk)));

	if (head->fd == fw) {
		return bin;
	}
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk) {
		GH(rz_heap_bin_free)
		(bin);
		return NULL;
	}
	GHT brk_start = GHT_MAX, brk_end = GHT_MAX, initial_brk = GHT_MAX;
	GH(get_brks)
	(core, &brk_start, &brk_end);
	if (brk_start == GHT_MAX || brk_end == GHT_MAX) {
		free(cnk);
		return bin;
	}
	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	int offset;
	GHT base;
	if (tcache) {
		offset = 16;
		const int fc_offset = rz_config_get_i(core->config, "dbg.glibc.fc_offset");
		base = m_arena + offset + SZ * (ut64)bin_num * 2 + 10 * SZ;
		initial_brk = ((brk_start >> 12) << 12) + fc_offset;
	} else {
		offset = 12 * SZ + sizeof(int) * 2;
		base = m_arena + offset + SZ * (ut64)bin_num * 2 - SZ * 2;
		initial_brk = (brk_start >> 12) << 12;
	}
	bin->addr = base;
	while (fw != head->fd) {
		if (fw > main_arena->top || fw < initial_brk) {
			bin->message = rz_str_dup("Corrupted list");
			break;
		}
		rz_io_read_at(core->io, fw, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
		RzHeapChunkListItem *chunk = RZ_NEW0(RzHeapChunkListItem);
		if (!chunk) {
			break;
		}
		chunk->addr = fw;
		rz_list_append(bin->chunks, chunk);
		fw = cnk->fd;
	}
	free(cnk);
	free(head);
	return bin;
}
/**
 * \brief Prints the heap chunks in a bin with double linked list (small|large|unsorted)
 * \param core RzCore pointer
 * \param main_arena MallocState struct for the arena in which bins are
 * \param bin_num The bin number for the bin from which chunks have to printed
 * \return number of chunks found in the bin
 */
static int GH(print_bin_content)(RzCore *core, MallocState *main_arena, int bin_num, PJ *pj, GHT m_arena) {
	RzListIter *iter;
	RzHeapChunkListItem *pos;
	RzHeapBin *bin = GH(rz_heap_bin_content)(core, main_arena, bin_num, m_arena);
	RzList *chunks = bin->chunks;
	if (rz_list_length(chunks) == 0) {
		GH(rz_heap_bin_free)
		(bin);
		return 0;
	}
	int chunks_cnt = 0;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (!pj) {
		rz_cons_printf("%s", bin->type);
		rz_cons_printf("_bin[");
		PRINTF_BA("%d", bin->bin_num);
		rz_cons_printf("]: fd=");
		PRINTF_YA("0x%" PFMT64x, bin->fd);
		rz_cons_printf(", bk=");
		PRINTF_YA("0x%" PFMT64x, bin->bk);
		rz_cons_printf(", base=");
		PRINTF_YA("0x%" PFMT64x, bin->addr);
		if (!strcmp(bin->type, "Small")) {
			rz_cons_printf(", size=");
			PRINTF_BA("0x%" PFMT64x, bin->size);
		}
		rz_cons_newline();
	} else {
		pj_kn(pj, "fd", bin->fd);
		pj_kn(pj, "bk", bin->bk);
		pj_kn(pj, "base", bin->addr);
		pj_ka(pj, "chunks");
	}
	rz_list_foreach (chunks, iter, pos) {
		if (!pj) {
			rz_cons_printf(" -> ");
		}
		GH(print_heap_chunk_simple)
		(core, pos->addr, NULL, pj);
		if (!pj) {
			rz_cons_newline();
		}
		chunks_cnt += 1;
	}
	if (bin->message) {
		PRINTF_RA("%s\n", bin->message);
	}
	GH(rz_heap_bin_free)
	(bin);
	if (pj) {
		pj_end(pj);
	}
	return chunks_cnt;
}

/**
 * \brief Prints unsorted bin description for an arena (used for `dmhd` command)
 * \param core RzCore pointer
 * \param m_arena Offset of the arena in memory
 * \param main_arena MallocState struct for the arena in which bin are
 */
static void GH(print_unsortedbin_description)(RzCore *core, GHT m_arena, MallocState *main_arena, PJ *pj) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (!pj) {
		rz_cons_printf("Unsorted bin in Arena @ ");
		PRINTF_YA("0x%" PFMT64x "\n", (ut64)m_arena);
	}
	if (pj) {
		pj_o(pj);
		pj_kn(pj, "bin_num", 0);
		pj_ks(pj, "bin_type", "unsorted");
	}
	int chunk_cnt = GH(print_bin_content)(core, main_arena, 0, pj, m_arena);
	if (!pj) {
		rz_cons_printf("Found %d chunks in unsorted bin\n", chunk_cnt);
	} else {
		pj_end(pj);
	}
}

/**
 * \brief Prints small bins description for an arena (used for `dmhd` command)
 * \param core RzCore pointer
 * \param m_arena Offset of the arena in memory
 * \param main_arena Pointer to MallocState struct for the arena in which bins are
 */
static void GH(print_smallbin_description)(RzCore *core, GHT m_arena, MallocState *main_arena, PJ *pj) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (!pj) {
		rz_cons_printf("Small bins in Arena @ ");
		PRINTF_YA("0x%" PFMT64x "\n", (ut64)m_arena);
	}
	int chunk_cnt = 0;
	int non_empty_cnt = 0;
	for (int bin_num = 1; bin_num < NSMALLBINS; bin_num++) {
		if (pj) {
			pj_o(pj);
			pj_kn(pj, "bin_num", bin_num);
			pj_ks(pj, "bin_type", "small");
		}
		int chunk_found = GH(print_bin_content)(core, main_arena, bin_num, pj, m_arena);
		if (pj) {
			pj_end(pj);
		}
		if (chunk_found > 0) {
			non_empty_cnt += 1;
		}
		chunk_cnt += chunk_found;
	}
	if (!pj) {
		rz_cons_printf("Found %d chunks in %d small bins\n", chunk_cnt, non_empty_cnt);
	}
}

/**
 * \brief Prints large bins description for an arena (used for `dmhd` command)
 * \param core RzCore pointer
 * \param m_arena Offset of the arena in memory
 * \param main_arena Pointer to MallocState struct for the arena in which bins are
 */
static void GH(print_largebin_description)(RzCore *core, GHT m_arena, MallocState *main_arena, PJ *pj) {
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	if (!pj) {
		rz_cons_printf("Large bins in Arena @ ");
		PRINTF_YA("0x%" PFMT64x "\n", (ut64)m_arena);
	}
	int chunk_cnt = 0;
	int non_empty_cnt = 0;
	for (int bin_num = NSMALLBINS; bin_num < NBINS - 2; bin_num++) {
		if (pj) {
			pj_o(pj);
			pj_kn(pj, "bin_num", bin_num);
			pj_ks(pj, "bin_type", "large");
		}
		int chunk_found = GH(print_bin_content)(core, main_arena, bin_num, pj, m_arena);
		if (pj) {
			pj_end(pj);
		}
		if (chunk_found > 0) {
			non_empty_cnt += 1;
		}
		chunk_cnt += chunk_found;
	}
	if (!pj) {
		rz_cons_printf("Found %d chunks in %d large bins\n", chunk_cnt, non_empty_cnt);
	}
}

/**
 * \brief Prints description of bins for main arena for `dmhd` command
 * \param core RzCore pointer
 * \param m_arena Offset of main arena in memory
 * \param main_arena Pointer to Malloc state struct for main arena
 * \param global_max_fast The largest fast bin size
 * \param format Enum to determine which type of bins to print.
 */
static void GH(print_main_arena_bins)(RzCore *core, GHT m_arena, MallocState *main_arena, GHT main_arena_base, GHT global_max_fast, RzHeapBinType format, bool json) {
	rz_return_if_fail(core && core->dbg && core->dbg->maps);
	PJ *pj = NULL;
	if (json) {
		pj = pj_new();
		if (!pj) {
			return;
		}
		pj_o(pj);
		pj_ka(pj, "bins");
	}
	if (format == RZ_HEAP_BIN_ANY || format == RZ_HEAP_BIN_TCACHE) {
		GH(print_tcache_content)
		(core, m_arena, main_arena_base, pj);
		rz_cons_newline();
	}
	if (format == RZ_HEAP_BIN_ANY || format == RZ_HEAP_BIN_FAST) {
		char *input = rz_str_newlen("", 1);
		bool main_arena_only = true;
		GH(print_heap_fastbin)
		(core, m_arena, main_arena, global_max_fast, input, main_arena_only, pj);
		free(input);
		rz_cons_newline();
	}
	if (format == RZ_HEAP_BIN_ANY || format == RZ_HEAP_BIN_UNSORTED) {
		GH(print_unsortedbin_description)
		(core, m_arena, main_arena, pj);
		rz_cons_newline();
	}
	if (format == RZ_HEAP_BIN_ANY || format == RZ_HEAP_BIN_SMALL) {
		GH(print_smallbin_description)
		(core, m_arena, main_arena, pj);
		rz_cons_newline();
	}
	if (format == RZ_HEAP_BIN_ANY || format == RZ_HEAP_BIN_LARGE) {
		GH(print_largebin_description)
		(core, m_arena, main_arena, pj);
		rz_cons_newline();
	}
	if (json) {
		pj_end(pj);
		pj_end(pj);
		rz_cons_println(pj_string(pj));
		pj_free(pj);
	}
}

void GH(rz_arena_list_free)(RzArenaListItem *item) {
	free(item->arena);
	free(item->type);
	free(item);
}
/**
 * \brief Get a list of RzArenaListItem structs for all the arenas
 * \param core RzCore pointer
 * \param m_arena Base address of MallocState struct of main arena
 * \param main_arena MallocState struct of main arena
 * \return RzList pointer for list of RzArenaListItem structs of all the arenas
 */
RZ_API RzList /*<RzArenaListItem *>*/ *GH(rz_heap_arenas_list)(RzCore *core, GHT m_arena, MallocState *main_arena) {
	RzList *arena_list = rz_list_newf((RzListFree)GH(rz_arena_list_free));
	MallocState *ta = RZ_NEW0(MallocState);
	if (!ta) {
		return arena_list;
	}
	// main arena
	if (!GH(rz_heap_update_main_arena)(core, m_arena, ta)) {
		free(ta);
		return arena_list;
	}
	RzArenaListItem *item = RZ_NEW0(RzArenaListItem);
	if (!item) {
		free(ta);
		return arena_list;
	}
	item->addr = m_arena;
	item->type = rz_str_dup("Main");
	item->arena = ta;
	rz_list_append(arena_list, item);
	if (main_arena->next != m_arena) {
		ta->next = main_arena->next;
		while (GH(is_arena)(core, m_arena, ta->next) && ta->next != m_arena) {
			ut64 ta_addr = ta->next;
			ta = RZ_NEW0(MallocState);
			if (!GH(rz_heap_update_main_arena)(core, ta_addr, ta)) {
				free(ta);
				return arena_list;
			}
			// thread arenas
			item = RZ_NEW0(RzArenaListItem);
			if (!item) {
				free(ta);
				break;
			}
			item->addr = ta_addr;
			item->type = rz_str_dup("Thread");
			item->arena = ta;
			rz_list_append(arena_list, item);
		}
	}
	return arena_list;
}

/**
 * \brief Get a list of all the heap chunks in an arena. The chunks are in form of a struct RzHeapChunkListItem
 * \param core RzCore pointer
 * \param main_arena MallocState struct of main arena
 * \param m_arena Base address of malloc state of main arena
 * \param m_state Base address of malloc state of the arena whose chunks are required
 * \param top_chunk Boolean value to return the top chunk in the list or not
 * \return RzList pointer for list of all chunks in a given arena
 */
RZ_API RzList /*<RzHeapChunkListItem *>*/ *GH(rz_heap_chunks_list)(RzCore *core, MallocState *main_arena,
	GHT m_arena, GHT m_state, bool top_chunk) {
	RzList *chunks = rz_list_newf((RzListFree)GH(rz_heap_chunk_free));
	if (!core || !core->dbg || !core->dbg->maps) {
		return chunks;
	}
	GHT global_max_fast = (64 * SZ / 4);
	GHT brk_start = GHT_MAX, brk_end = GHT_MAX, size_tmp, min_size = SZ * 4;
	GHT tcache_fd = GHT_MAX, tcache_tmp = GHT_MAX;
	GHT initial_brk = GHT_MAX, tcache_initial_brk = GHT_MAX;

	const int tcache = rz_config_get_i(core->config, "dbg.glibc.tcache");
	const int offset = rz_config_get_i(core->config, "dbg.glibc.fc_offset");
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	int glibc_version = core->dbg->glibc_version;

	if (m_arena == m_state) {
		GH(get_brks)
		(core, &brk_start, &brk_end);
		if (tcache) {
			initial_brk = ((brk_start >> 12) << 12) + GH(HDR_SZ);
			if (rz_config_get_b(core->config, "cfg.debug")) {
				tcache_initial_brk = initial_brk;
			}
			initial_brk += (glibc_version < 230)
				? sizeof(GH(RzHeapTcachePre230))
				: sizeof(GH(RzHeapTcache));
		} else {
			initial_brk = (brk_start >> 12) << 12;
		}
	} else {
		brk_start = ((m_state >> 16) << 16);
		brk_end = brk_start + main_arena->system_mem;
		if (tcache) {
			tcache_initial_brk = brk_start + sizeof(GH(RzHeapInfo)) + sizeof(GH(RzHeap_MallocState_tcache)) + GH(MMAP_ALIGN);
			initial_brk = tcache_initial_brk + offset;
		} else {
			initial_brk = brk_start + sizeof(GH(RzHeapInfo)) + sizeof(GH(RzHeap_MallocState)) + MMAP_OFFSET;
		}
	}

	if (brk_start == GHT_MAX || brk_end == GHT_MAX || initial_brk == GHT_MAX) {
		RZ_LOG_ERROR("core: no heap section\n");
		return chunks;
	}

	GHT next_chunk = initial_brk, prev_chunk = next_chunk;
	GH(RzHeapChunk) *cnk = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk) {
		return chunks;
	}
	GH(RzHeapChunk) *cnk_next = RZ_NEW0(GH(RzHeapChunk));
	if (!cnk_next) {
		free(cnk);
		return chunks;
	}

	(void)rz_io_read_at(core->io, next_chunk, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
	size_tmp = (cnk->size >> 3) << 3;
	ut64 prev_chunk_addr;
	ut64 prev_chunk_size;
	while (next_chunk && next_chunk >= brk_start && next_chunk < main_arena->top) {
		if (size_tmp < min_size || next_chunk + size_tmp > main_arena->top) {
			RzHeapChunkListItem *block = RZ_NEW0(RzHeapChunkListItem);
			if (!block) {
				break;
			}
			block->addr = next_chunk;
			block->status = rz_str_dup("corrupted");
			block->size = size_tmp;
			rz_list_append(chunks, block);
			break;
		}

		prev_chunk_addr = (ut64)prev_chunk;
		prev_chunk_size = (((ut64)cnk->size) >> 3) << 3;
		bool fastbin = size_tmp >= SZ * 4 && size_tmp <= global_max_fast;
		bool is_free = false, double_free = false;

		if (fastbin) {
			int i = (size_tmp / (SZ * 2)) - 2;
			GHT idx = (GHT)main_arena->fastbinsY[i];
			(void)rz_io_read_at(core->io, idx, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
			GHT next = GH(get_next_pointer)(core, idx, cnk->fd);
			if (prev_chunk == idx && idx && !next) {
				is_free = true;
			}
			while (next && next >= brk_start && next < main_arena->top) {
				if (prev_chunk == idx || prev_chunk == next || idx == next) {
					is_free = true;
					if (idx == next) {
						double_free = true;
						break;
					}
					(void)rz_io_read_at(core->io, next, (ut8 *)cnk_next, sizeof(GH(RzHeapChunk)));
					GHT next_node = GH(get_next_pointer)(core, next, cnk_next->fd);
					// avoid triple while?
					while (next_node && next_node >= brk_start && next_node < main_arena->top) {
						if (prev_chunk == next_node) {
							double_free = true;
							break;
						}
						(void)rz_io_read_at(core->io, next_node, (ut8 *)cnk_next, sizeof(GH(RzHeapChunk)));
						next_node = GH(get_next_pointer)(core, next_node, cnk_next->fd);
					}
					if (double_free) {
						break;
					}
				}
				(void)rz_io_read_at(core->io, next, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
				next = GH(get_next_pointer)(core, next, cnk->fd);
			}
			if (double_free) {
				PRINT_RA(" Double free in simple-linked list detected ");
				break;
			}
			prev_chunk_size = ((i + 1) * GH(HDR_SZ)) + GH(HDR_SZ);
		}

		if (tcache) {
			GH(RTcache) *tcache_heap = GH(tcache_new)(core);
			if (!tcache_heap) {
				free(cnk);
				free(cnk_next);
				return chunks;
			}
			GH(tcache_read)
			(core, tcache_initial_brk, tcache_heap);
			size_t i;
			for (i = 0; i < TCACHE_MAX_BINS; i++) {
				int count = GH(tcache_get_count)(tcache_heap, i);
				GHT entry = GH(tcache_get_entry)(tcache_heap, i);
				if (count > 0) {
					if (entry - SZ * 2 == prev_chunk) {
						is_free = true;
						prev_chunk_size = ((i + 1) * TC_HDR_SZ + GH(TC_SZ));
						break;
					}
					if (count > 1) {
						tcache_fd = entry;
						int n;
						for (n = 1; n < count; n++) {
							bool r = rz_io_read_at(core->io, tcache_fd, (ut8 *)&tcache_tmp, sizeof(GHT));
							if (!r) {
								break;
							}
							tcache_tmp = GH(get_next_pointer)(core, tcache_fd, read_le(&tcache_tmp));
							if (tcache_tmp - SZ * 2 == prev_chunk) {
								is_free = true;
								prev_chunk_size = ((i + 1) * TC_HDR_SZ + GH(TC_SZ));
								break;
							}
							tcache_fd = (ut64)tcache_tmp;
						}
					}
				}
			}
			GH(tcache_free)
			(tcache_heap);
		}

		next_chunk += size_tmp;
		prev_chunk = next_chunk;
		rz_io_read_at(core->io, next_chunk, (ut8 *)cnk, sizeof(GH(RzHeapChunk)));
		size_tmp = (cnk->size >> 3) << 3;
		RzHeapChunkListItem *block = RZ_NEW0(RzHeapChunkListItem);
		if (!block) {
			break;
		}
		char *status = rz_str_dup("allocated");
		if (fastbin) {
			if (is_free) {
				strcpy(status, "free");
			}
		}
		if (!(cnk->size & 1)) {
			strcpy(status, "free");
		}
		if (tcache) {
			if (is_free) {
				strcpy(status, "free");
			}
		}
		block->addr = prev_chunk_addr;
		block->status = status;
		block->size = prev_chunk_size;
		rz_list_append(chunks, block);
	}
	if (top_chunk) {
		RzHeapChunkListItem *block = RZ_NEW0(RzHeapChunkListItem);
		if (block) {
			block->addr = main_arena->top;
			block->status = rz_str_dup("free (top)");
			RzHeapChunkSimple *chunkSimple = GH(rz_heap_chunk_wrapper)(core, main_arena->top);
			if (chunkSimple) {
				block->size = chunkSimple->size;
				free(chunkSimple);
			}
			rz_list_append(chunks, block);
		}
	}
	free(cnk);
	free(cnk_next);
	return chunks;
}

RZ_IPI RzCmdStatus GH(rz_cmd_arena_print_handler)(RzCore *core, int argc, const char **argv) {
	GHT m_arena = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_arena, main_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	RzList *arenas_list = GH(rz_heap_arenas_list)(core, m_arena, main_arena);
	RzListIter *iter;
	RzArenaListItem *pos;
	bool flag = false;
	rz_list_foreach (arenas_list, iter, pos) {
		MallocState *arena = pos->arena;
		if (!flag) {
			flag = true;
			rz_cons_printf("Main arena  (addr=");
		} else {
			rz_cons_printf("Thread arena(addr=");
		}
		PRINTF_YA("0x%" PFMT64x, (ut64)pos->addr);
		rz_cons_printf(", lastRemainder=");
		PRINTF_YA("0x%" PFMT64x, (ut64)arena->last_remainder);
		rz_cons_printf(", top=");
		PRINTF_YA("0x%" PFMT64x, (ut64)arena->top);
		rz_cons_printf(", next=");
		PRINTF_YA("0x%" PFMT64x, (ut64)arena->next);
		if (arena->attached_threads) {
			rz_cons_printf(")\n");
		} else {
			rz_cons_printf(", free)\n");
		}
	}
	rz_list_free(arenas_list);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_chunks_print_handler)(RzCore *core, int argc, const char **argv, RzCmdStateOutput *state) {
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	MallocState *main_arena = RZ_NEW0(MallocState);
	RzOutputMode mode = state->mode;
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (argc == 1) {
		m_state = m_arena;
	} else if (argc == 2) {
		m_state = rz_num_get(NULL, argv[1]);
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		free(main_arena);
		PRINT_RA("This address is not a valid arena\n");
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	GHT brk_start, brk_end;
	if (m_arena == m_state) {
		GH(get_brks)
		(core, &brk_start, &brk_end);

	} else {
		brk_start = ((m_state >> 16) << 16);
		brk_end = brk_start + main_arena->system_mem;
	}
	RzListIter *iter;
	RzHeapChunkListItem *pos;
	PJ *pj = state->d.pj;
	int w, h;
	RzConfigHold *hc = rz_config_hold_new(core->config);
	if (!hc) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	w = rz_cons_get_size(&h);
	RzConsCanvas *can = rz_cons_canvas_new(w, h);
	if (!can) {
		free(main_arena);
		rz_config_hold_free(hc);
		return RZ_CMD_STATUS_ERROR;
	}

	RzAGraph *g = rz_agraph_new(can);
	if (!g) {
		free(main_arena);
		rz_cons_canvas_free(can);
		rz_config_hold_restore(hc);
		rz_config_hold_free(hc);
		return RZ_CMD_STATUS_ERROR;
	}
	RzANode *top = RZ_EMPTY, *chunk_node = RZ_EMPTY, *prev_node = RZ_EMPTY;
	char *top_title = NULL, *top_data = NULL, *node_title = NULL, *node_data = NULL;
	bool first_node = true;
	top_data = rz_str_dup("");
	RzList *chunks = GH(rz_heap_chunks_list)(core, main_arena, m_arena, m_state, false);
	if (mode == RZ_OUTPUT_MODE_JSON) {
		if (!pj) {
			goto end;
		}
		pj_o(pj);
		pj_ka(pj, "chunks");
	} else if (mode == RZ_OUTPUT_MODE_STANDARD || mode == RZ_OUTPUT_MODE_LONG) {
		rz_cons_printf("Arena @ ");
		PRINTF_YA("0x%" PFMT64x, (ut64)m_state);
		rz_cons_newline();
	} else if (mode == RZ_OUTPUT_MODE_LONG_JSON) {
		can->linemode = rz_config_get_i(core->config, "graph.linemode");
		can->color = rz_config_get_i(core->config, "scr.color");
		core->cons->use_utf8 = rz_config_get_i(core->config, "scr.utf8");
		g->layout = rz_config_get_i(core->config, "graph.layout");
		rz_agraph_set_title(g, "Heap Layout");
		top_title = rz_str_newf("Top chunk @ 0x%" PFMT64x "\n", (ut64)main_arena->top);
	}
	rz_list_foreach (chunks, iter, pos) {
		if (mode == RZ_OUTPUT_MODE_STANDARD || mode == RZ_OUTPUT_MODE_LONG) {
			GH(print_heap_chunk_simple)
			(core, pos->addr, pos->status, NULL);
			rz_cons_newline();
			if (mode == RZ_OUTPUT_MODE_LONG) {
				int size = 0x10;
				char *data = calloc(1, size);
				if (data) {
					rz_io_nread_at(core->io, (ut64)(pos->addr + SZ * 2), (ut8 *)data, size);
					core->print->flags &= ~RZ_PRINT_FLAGS_HEADER;
					core->print->pairs = false;
					rz_cons_printf("   ");
					rz_core_print_hexdump(core, (ut64)(pos->addr + SZ * 2), (ut8 *)data, size, SZ * 2, 1, 1);
					core->print->flags |= RZ_PRINT_FLAGS_HEADER;
					core->print->pairs = true;
					free(data);
				}
			}
		} else if (mode == RZ_OUTPUT_MODE_JSON) {
			pj_o(pj);
			pj_kn(pj, "addr", pos->addr);
			pj_kn(pj, "size", pos->size);
			pj_ks(pj, "status", pos->status);
			pj_end(pj);
		} else if (mode == RZ_OUTPUT_MODE_RIZIN) {
			rz_cons_printf("fs heap.%s\n", pos->status);
			char *name = rz_str_newf("chunk.%06" PFMT64x, ((pos->addr >> 4) & 0xffffULL));
			rz_cons_printf("f %s %d @ 0x%" PFMT64x "\n", name, (int)pos->size, (ut64)pos->addr);
			free(name);
		} else if (mode == RZ_OUTPUT_MODE_LONG_JSON) { // graph
			free(node_title);
			free(node_data);
			node_title = rz_str_newf("  Malloc chunk @ 0x%" PFMT64x " ", (ut64)pos->addr);
			node_data = rz_str_newf("size: 0x%" PFMT64x " status: %s\n", (ut64)pos->size, pos->status);
			chunk_node = rz_agraph_add_node(g, node_title, node_data);
			if (first_node) {
				first_node = false;
			} else {
				rz_agraph_add_edge(g, prev_node, chunk_node);
			}
			prev_node = chunk_node;
		}
	}
	if (mode == RZ_OUTPUT_MODE_STANDARD || mode == RZ_OUTPUT_MODE_LONG) {
		GH(print_heap_chunk_simple)
		(core, main_arena->top, "free", NULL);
		PRINT_RA("[top]");
		rz_cons_printf("[brk_start: ");
		PRINTF_YA("0x%" PFMT64x, (ut64)brk_start);
		rz_cons_printf(", brk_end: ");
		PRINTF_YA("0x%" PFMT64x, (ut64)brk_end);
		rz_cons_printf("]");
	} else if (mode == RZ_OUTPUT_MODE_JSON) {
		pj_end(pj);
		pj_kn(pj, "top", main_arena->top);
		pj_kn(pj, "brk", brk_start);
		pj_kn(pj, "end", brk_end);
		pj_end(pj);
	} else if (mode == RZ_OUTPUT_MODE_RIZIN) {
		rz_cons_printf("fs-\n");
		rz_cons_printf("f heap.top @ 0x%08" PFMT64x "\n", (ut64)main_arena->top);
		rz_cons_printf("f heap.brk @ 0x%08" PFMT64x "\n", (ut64)brk_start);
		rz_cons_printf("f heap.end @ 0x%08" PFMT64x "\n", (ut64)brk_end);
	} else if (mode == RZ_OUTPUT_MODE_LONG_JSON) {
		top = rz_agraph_add_node(g, top_title, top_data);
		if (!first_node) {
			rz_agraph_add_edge(g, prev_node, top);
			free(node_data);
			free(node_title);
		}
		rz_agraph_print(g);
	}
end:
	rz_cons_newline();
	free(g);
	free(top_data);
	free(top_title);
	rz_list_free(chunks);
	free(main_arena);
	rz_cons_canvas_free(can);
	rz_config_hold_restore(hc);
	rz_config_hold_free(hc);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_main_arena_print_handler)(RzCore *core, int argc, const char **argv, RzOutputMode mode) {
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	GHT global_max_fast = (64 * SZ / 4);
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (argc == 1) {
		m_state = m_arena;
	} else if (argc == 2) {
		m_state = rz_num_get(NULL, argv[1]);
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		PRINT_RA("This address is not a valid arena\n");
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	GH(print_arena_stats)
	(core, m_state, main_arena, global_max_fast, mode);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_chunk_print_handler)(RzCore *core, int argc, const char **argv) {
	GHT m_arena = GHT_MAX;
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	ut64 addr = core->offset;
	GH(print_heap_chunk)
	(core, addr);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_info_print_handler)(RzCore *core, int argc, const char **argv) {
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (argc == 1) {
		m_state = m_arena;
	} else if (argc == 2) {
		m_state = rz_num_get(NULL, argv[1]);
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		PRINT_RA("This address is not a valid arena\n");
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	GH(print_malloc_info)
	(core, m_arena, m_state);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_tcache_print_handler)(RzCore *core, int argc, const char **argv) {
	GHT m_arena = GHT_MAX;

	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		return RZ_CMD_STATUS_ERROR;
	}

	const int tc = rz_config_get_i(core->config, "dbg.glibc.tcache");
	if (!tc) {
		rz_cons_printf("No tcache present in this version of libc\n");
		return RZ_CMD_STATUS_ERROR;
	}

	if (!GH(resolve_heap_tcache)(core, m_arena)) {
		return RZ_CMD_STATUS_ERROR;
	}

	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_bins_list_print_handler)(RzCore *core, const char *input, RzOutputMode mode) {
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	char *m_state_str, *dup = rz_str_dup(input);
	if (*dup) {
		strtok(dup, ":");
		m_state_str = strtok(NULL, ":");
		m_state = rz_num_get(NULL, m_state_str);
		if (!m_state) {
			m_state = m_arena;
		}
	} else {
		if (core->offset != core->prompt_offset) {
			m_state = core->offset;
		} else {
			m_state = m_arena;
		}
	}
	if (GH(is_arena)(core, m_arena, m_state)) {
		if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
			free(main_arena);
			free(dup);
			return RZ_CMD_STATUS_ERROR;
		}
		GH(print_heap_bin)
		(core, m_state, main_arena, dup, mode == RZ_OUTPUT_MODE_GRAPH);
	} else {
		PRINT_RA("This address is not part of the arenas\n");
		free(main_arena);
		free(dup);
		return RZ_CMD_STATUS_ERROR;
	}
	free(dup);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_fastbins_print_handler)(void *data, const char *input) {
	RzCore *core = (RzCore *)data;
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	MallocState *main_arena = RZ_NEW0(MallocState);
	GHT global_max_fast = (64 * SZ / 4);
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	bool main_arena_only = false;
	char *m_state_str, *dup = rz_str_dup(input);
	if (*dup) {
		strtok(dup, ":");
		m_state_str = strtok(NULL, ":");
		m_state = rz_num_get(NULL, m_state_str);
		if (!m_state) {
			m_state = m_arena;
		}
	} else {
		if (core->offset != core->prompt_offset) {
			m_state = core->offset;
		} else {
			m_state = m_arena;
		}
	}
	if (GH(is_arena)(core, m_arena, m_state)) {
		if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
			free(dup);
			free(main_arena);
			return RZ_CMD_STATUS_ERROR;
		}
		GH(print_heap_fastbin)
		(core, m_state, main_arena, global_max_fast, dup, main_arena_only, NULL);
	} else {
		PRINT_RA("This address is not part of the arenas\n");
		free(dup);
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	free(dup);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

RZ_IPI RzCmdStatus GH(rz_cmd_heap_arena_bins_print_handler)(RzCore *core, int argc, const char **argv, RzOutputMode mode) {
	GHT m_arena = GHT_MAX, m_state = GHT_MAX;
	RzConsPrintablePalette *pal = &rz_cons_singleton()->context->pal;
	GHT global_max_fast = (64 * SZ / 4);
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (core->offset != core->prompt_offset) {
		m_state = core->offset;
	} else {
		m_state = m_arena;
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		PRINT_RA("This address is not part of the arenas\n");
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return RZ_CMD_STATUS_ERROR;
	}

	bool json = false;
	if (mode == RZ_OUTPUT_MODE_JSON) { // dmhdj
		json = true;
	}
	RzHeapBinType bin_format = RZ_HEAP_BIN_ANY;
	if (argc == 2) {
		const char *input = argv[1];
		if (!strcmp(input, "tcache")) {
			bin_format = RZ_HEAP_BIN_TCACHE;
		} else if (!strcmp(input, "fast")) {
			bin_format = RZ_HEAP_BIN_FAST;
		} else if (!strcmp(input, "unsorted")) {
			bin_format = RZ_HEAP_BIN_UNSORTED;
		} else if (!strcmp(input, "small")) {
			bin_format = RZ_HEAP_BIN_SMALL;
		} else if (!strcmp(input, "large")) {
			bin_format = RZ_HEAP_BIN_LARGE;
		}
	}
	GH(print_main_arena_bins)
	(core, m_state, main_arena, m_arena, global_max_fast, bin_format, json);
	free(main_arena);
	return RZ_CMD_STATUS_OK;
}

/**
 * \brief A wrapper around GH(rz_heap_arenas_list) which handles finding main_arena
 * \param core RzCore pointer
 * \return RzList of RzArenaListItem
 */
RZ_API RzList /*<RzArenaListItem *>*/ *GH(rz_heap_arena_list_wrapper)(RzCore *core) {
	GHT m_arena;
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		return rz_list_newf(free);
	}
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return rz_list_newf(free);
	}
	if (!GH(rz_heap_update_main_arena)(core, m_arena, main_arena)) {
		free(main_arena);
		return rz_list_newf(free);
	}
	RzList *arenas_list = GH(rz_heap_arenas_list)(core, m_arena, main_arena);
	free(main_arena);
	return arenas_list;
}

/**
 * \brief A wrapper around GH(rz_heap_chunks_list) which handles finding the main arena
 * \param core RzCore pointer
 * \param m_arena Base Address of the arena
 * \return RzList of heap chunks as RzHeapChunkListItem structs
 */
RZ_API RzList /*<RzHeapChunkListItem *>*/ *GH(rz_heap_chunks_list_wrapper)(RzCore *core, ut64 m_state) {
	GHT m_arena;
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		return rz_list_newf(free);
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		return rz_list_newf(free);
	}
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return rz_list_newf(free);
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return rz_list_newf(free);
	}
	RzList *chunks = GH(rz_heap_chunks_list)(core, main_arena, m_arena, m_state, true);
	free(main_arena);
	return chunks;
}

/**
 * \brief Get info about a heap chunk as RzHeapChunkSimple
 * \param core RzCore pointer
 * \param addr Base address of the heap chunk
 * \return RzHeapChunkSimple struct pointer for the chunk
 */
RZ_API RzHeapChunkSimple *GH(rz_heap_chunk_wrapper)(RzCore *core, GHT addr) {
	GH(RzHeapChunk) *heap_chunk = GH(rz_heap_get_chunk_at_addr)(core, addr);
	if (!heap_chunk) {
		return NULL;
	}
	RzHeapChunkSimple *simple_chunk = RZ_NEW0(RzHeapChunkSimple);
	if (!simple_chunk) {
		free(heap_chunk);
		return NULL;
	}
	ut64 size = heap_chunk->size;
	simple_chunk->addr = addr;
	simple_chunk->size = size & ~(NON_MAIN_ARENA | IS_MMAPPED | PREV_INUSE);
	simple_chunk->non_main_arena = (bool)((size & NON_MAIN_ARENA) >> 2);
	simple_chunk->is_mmapped = (bool)((size & IS_MMAPPED) >> 1);
	simple_chunk->prev_inuse = (bool)(size & PREV_INUSE);
	simple_chunk->prev_size = heap_chunk->prev_size;
	simple_chunk->bk = heap_chunk->bk;
	simple_chunk->fd = heap_chunk->fd;
	simple_chunk->fd_nextsize = heap_chunk->fd_nextsize;
	simple_chunk->bk_nextsize = heap_chunk->bk_nextsize;
	free(heap_chunk);
	return simple_chunk;
}

/**
 * \brief Get MallocState struct for arena with given base address
 * if base address is 0 then return MallocState for main arena
 * \param core RzCore pointer
 * \param addr Base address of the arena
 * \return MallocState struct pointer for the arena
 */
RZ_API MallocState *GH(rz_heap_get_arena)(RzCore *core, GHT m_state) {
	GHT m_arena;
	if (!GH(rz_heap_resolve_main_arena)(core, &m_arena)) {
		return NULL;
	}
	if (!m_state) {
		m_state = m_arena;
	}
	if (!GH(is_arena)(core, m_arena, m_state)) {
		return NULL;
	}
	MallocState *main_arena = RZ_NEW0(MallocState);
	if (!main_arena) {
		return NULL;
	}
	if (!GH(rz_heap_update_main_arena)(core, m_state, main_arena)) {
		free(main_arena);
		return NULL;
	}
	return main_arena;
}

/**
 * \brief Write a heap chunk header to memory
 * \param core RzCore pointer
 * \param chunk_simple RzHeapChunkSimple pointer to the heap chunk data
 * \return bool if the write succeeded or not
 */
RZ_API bool GH(rz_heap_write_heap_chunk)(RzCore *core, RzHeapChunkSimple *chunk_simple) {
	if (!chunk_simple) {
		return false;
	}
	GH(RzHeapChunk) *heap_chunk = RZ_NEW0(GH(RzHeapChunk));
	if (!heap_chunk) {
		return false;
	}

	heap_chunk->size = chunk_simple->size;
	// add flag bits to chunk size
	if (chunk_simple->prev_inuse) {
		heap_chunk->size |= PREV_INUSE;
	}
	if (chunk_simple->is_mmapped) {
		heap_chunk->size |= IS_MMAPPED;
	}
	if (chunk_simple->non_main_arena) {
		heap_chunk->size |= NON_MAIN_ARENA;
	}

	heap_chunk->fd = chunk_simple->fd;
	heap_chunk->bk = chunk_simple->bk;
	heap_chunk->fd_nextsize = chunk_simple->fd_nextsize;
	heap_chunk->bk_nextsize = chunk_simple->bk_nextsize;
	bool res = rz_io_write_at(core->io, chunk_simple->addr, (ut8 *)heap_chunk, sizeof(GH(RzHeapChunk)));
	free(heap_chunk);
	return res;
}
