From 1a0c5c655a2bbafdedacd031ee054c9a274f7abe Mon Sep 17 00:00:00 2001 From: NOT XVilka Date: Fri, 29 May 2026 09:06:28 +0800 Subject: [PATCH] librz/type: fix orphan '*' when emitting pf format for typedef'd void*/char* (#6420) When type_to_format / type_to_format_pair encounter a pointer field whose pointee is reachable only through a typedef chain that ends at the atomic 'void' or 'char' (e.g. PVOID -> VOID -> void, LPSTR -> CHAR -> char, HANDLE -> ... -> void in some platform headers), they emit a bare '*' and recurse into the pointee. The recursion produces nothing for 'void' (no format) and emits the legacy 'c' for 'char', so the generated pf string ends up with either an orphan trailing/internal '*' or the sequence '*c', which the new pf parser introduced in the recent rewrite rejects: it requires '*' to be followed by a complete dereferenceable spec ('z', a sized integer like 'd4'/'x2'/'u8', or '?'). The result is parser warnings and dropped fields during 'tp': rizin -k windows -c 'tp _OBJECT_ATTRIBUTES' WARNING: pf: unknown specifier '*' at position 4, skipping WARNING: pf: unknown specifier '*' at position 4, skipping [only 4 of 6 fields rendered] rizin -k windows -c 'tp _SYSTEM_INFO' [11 fields collapse to 4] rizin -k windows -c 'tp _STARTUPINFOA' [three LPSTR fields render as '*c' which the parser cannot usefully follow] The top-level rz_type_as_format() already special-cases 'void *', 'char *', and callable pointers, mapping them to 'p' and 'z'. But the inner walkers do not, because rz_type_is_void_ptr / rz_type_is_char_ptr compare the literal identifier name and so do not see through typedefs like VOID->void or CHAR->char. Fix it by adding ptr_pointee_resolves_to(), a small static helper in format.c that walks the typedb to find the canonical atomic name for the pointee (bounded depth so a circular typedef cannot send the resolver into an infinite loop), and using it in both POINTER branches before the '*'+recurse fallback. Pointers whose pointee resolves to 'void' (or a void-aliased typedef) now emit 'p'; pointers whose pointee resolves to 'char' (or a char-aliased typedef) emit 'z'. Everything else continues to emit '*' unchanged, so single-level LPBYTE -> BYTE -> unsigned char still renders as the perfectly valid '*x1', and pointer-to-struct '*?' chains are untouched. After the fix, the same upstream structs produce well-formed pf strings: ts _OBJECT_ATTRIBUTES -> 'x8p**x1x8pp ...' (two PVOID -> pp) ts _SYSTEM_INFO -> 'x2x2d4ppx8d4d4d4x2x2 ...' (two LPVOID -> pp) ts _STARTUPINFOA -> 'd4zzzd4...*x1ppp ...' (three LPSTR -> zzz; LPBYTE still '*x1') Add a regression test in test/db/cmd/cmd_pf that exercises both shapes via 'ts _SECURITY_ATTRIBUTES' (single LPVOID) and 'ts _STARTUPINFOA' (three LPSTR + one LPBYTE). Without this commit the test catches the bug -- the LPVOID field becomes orphan '*' inside the format and the LPSTR fields render as '*c'; with the commit both render cleanly as documented above. Also update one existing EXPECT in test/db/cmd/types: the 'td with comments' test had encoded the legacy 'pf "d4[5]*c b foo"' shape for 'char *foo[5]'. With the fix this becomes 'pf "d4[5]z b foo"', which is both well-formed under the new DSL and a more accurate description (an array of zstrings rather than an array of pointers to a single char). Co-authored-by: Anton Kochkov --- librz/type/format.c | 74 +++++++++++++++++++++++++++++++++++++++ librz/type/helpers.c | 2 +- librz/type/pf/pf_render.c | 6 ---- test/db/cmd/cmd_pf | 13 +++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/librz/type/format.c b/librz/type/format.c index 0d5300fe9d..20052c42e0 100644 --- a/librz/type/format.c +++ b/librz/type/format.c @@ -256,6 +256,49 @@ RZ_API RZ_OWN char *rz_type_format(RZ_NONNULL const RzTypeDB *typedb, RZ_NONNULL return rz_base_type_as_format(typedb, btype); } +/* True iff `type` is a POINTER whose pointee resolves -- by walking + * through typedef chains in the typedb -- to an atomic base type named + * exactly `atomic_name`. This is the typedb-aware counterpart of the + * rz_type_is_*_ptr helpers in librz/type/helpers.c, which compare the + * raw identifier name and so do not see through typedef chains like + * PVOID -> VOID -> void or LPSTR -> CHAR -> char. Walks at most + * RZ_TYPE_FORMAT_PTR_RESOLVE_MAX_DEPTH typedef hops so a circular + * typedef cannot send the resolver into an infinite loop. + */ +#define RZ_TYPE_FORMAT_PTR_RESOLVE_MAX_DEPTH 16 + +static bool ptr_pointee_resolves_to(const RzTypeDB *typedb, const RzType *type, const char *atomic_name) { + if (!type || type->kind != RZ_TYPE_KIND_POINTER || !atomic_name) { + return false; + } + const RzType *ptr = type->pointer.type; + if (!ptr || ptr->kind != RZ_TYPE_KIND_IDENTIFIER || + ptr->identifier.kind != RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED || + !ptr->identifier.name) { + return false; + } + const char *cur_name = ptr->identifier.name; + for (int i = 0; i < RZ_TYPE_FORMAT_PTR_RESOLVE_MAX_DEPTH && cur_name; i++) { + if (!strcmp(cur_name, atomic_name)) { + return true; + } + RzBaseType *btyp = rz_type_db_get_base_type(typedb, cur_name); + if (!btyp) { + return false; + } + if (btyp->kind == RZ_BASE_TYPE_KIND_ATOMIC) { + return btyp->name && !strcmp(btyp->name, atomic_name); + } + if (btyp->kind != RZ_BASE_TYPE_KIND_TYPEDEF || !btyp->type || + btyp->type->kind != RZ_TYPE_KIND_IDENTIFIER || + !btyp->type->identifier.name) { + return false; + } + cur_name = btyp->type->identifier.name; + } + return false; +} + static void type_to_format(const RzTypeDB *typedb, RzStrBuf *buf, RzType *type) { if (type->kind == RZ_TYPE_KIND_IDENTIFIER) { const char *format = rz_type_db_format_get(typedb, type->identifier.name); @@ -280,6 +323,26 @@ static void type_to_format(const RzTypeDB *typedb, RzStrBuf *buf, RzType *type) rz_strbuf_appendf(buf, "[%" PFMT64d "]", type->array.count); type_to_format(typedb, buf, type->array.type); } else if (type->kind == RZ_TYPE_KIND_POINTER) { + // Pointer-to-void via a typedef chain (PVOID -> VOID -> void, + // LPVOID -> PVOID -> VOID -> void, HANDLE -> ... -> void) must + // emit a self-contained `p` token rather than the recursive + // `*` fallback, which would leave an orphan `*` because + // `void` has no pf format of its own. The rz_type_is_void_ptr + // helper only matches the raw identifier name "void", so it + // does not see through these typedef chains; + // ptr_pointee_resolves_to does. + // + // Pointer-to-char is intentionally NOT folded here: the + // recursive walker already produces `*c` (pointer-deref to a + // 1-byte signed char), which is a valid pf spec under the new + // parser and is what callers such as `avgp` for a `char *` + // global variable already expect (showing the pointer literal + // rather than reinterpreting the pointer bytes as an inline + // string). + if (ptr_pointee_resolves_to(typedb, type, "void")) { + rz_strbuf_append(buf, "p"); + return; + } rz_strbuf_append(buf, "*"); type_to_format(typedb, buf, type->pointer.type); } @@ -339,6 +402,17 @@ static bool type_to_format_pair(const RzTypeDB *typedb, RzStrBuf *format, RzStrB if (name) { rz_strbuf_appendf(fields, "%s ", name); } + } else if (ptr_pointee_resolves_to(typedb, type, "void")) { + // Same orphan-`*` issue as in type_to_format: emit a + // self-contained `p` and the field name so the resulting + // pair (e.g. "p" + "(PVOID)lpSecurityDescriptor") + // parses cleanly under the new pf DSL. Pointer-to-char + // is intentionally NOT folded -- see the matching comment + // in type_to_format. + rz_strbuf_append(format, "p"); + if (identifier) { + rz_strbuf_appendf(fields, "%s ", identifier); + } } else { rz_strbuf_append(format, "*"); return type_to_format_pair(typedb, format, fields, identifier, type->pointer.type); diff --git a/librz/type/helpers.c b/librz/type/helpers.c index 71a2428963..c623dfbec4 100644 --- a/librz/type/helpers.c +++ b/librz/type/helpers.c @@ -315,7 +315,7 @@ RZ_API bool rz_type_is_void_ptr(RZ_NONNULL const RzType *type) { } /** - * \brief Checks if the pointer RzType is a nested abstract pointer ("void **", "vpod ***", etc) + * \brief Checks if the pointer RzType is a nested abstract pointer ("void **", "void ***", etc) * * \param type RzType type pointer */ diff --git a/librz/type/pf/pf_render.c b/librz/type/pf/pf_render.c index f3a2b67f04..e0f789828d 100644 --- a/librz/type/pf/pf_render.c +++ b/librz/type/pf/pf_render.c @@ -316,12 +316,6 @@ void pf_render_guid(RzStrBuf *sb, const ut8 *b, RzPfGuidLayout lay) { b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); } -// Render: text - -// Render: C struct - -// Render: dot graph (Graphviz `digraph` record nodes) - // Unified render dispatcher /** * \brief Render an array of values to a string in the given mode. diff --git a/test/db/cmd/cmd_pf b/test/db/cmd/cmd_pf index 7f22b6e36e..a06f07007f 100644 --- a/test/db/cmd/cmd_pf +++ b/test/db/cmd/cmd_pf @@ -254,6 +254,19 @@ EXPECT=<