rizin/librz/type
NOT XVilka 1a0c5c655a
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 '*<inner>' 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 <anton.kockov@gmail.com>
2026-05-29 09:06:28 +08:00
..
parser analysis: add enum immediate hints with ahie command (#6052) 2026-03-21 15:52:21 +08:00
pf librz/type: fix orphan '*' when emitting pf format for typedef'd void*/char* (#6420) 2026-05-29 09:06:28 +08:00
base.c Fix memory leaks related to DWARF (#6245) 2026-04-17 17:33:11 +00:00
format.c librz/type: fix orphan '*' when emitting pf format for typedef'd void*/char* (#6420) 2026-05-29 09:06:28 +08:00
function.c Convert %ll format specifiers to PFMT64 (#6267) 2026-04-22 05:57:04 +08:00
helpers.c librz/type: fix orphan '*' when emitting pf format for typedef'd void*/char* (#6420) 2026-05-29 09:06:28 +08:00
meson.build Rewrite the pf parser and rework the grammar (#6410) 2026-05-29 03:58:58 +08:00
path.c Fix the missing release of RZ_OWN parameters (#5641) 2025-12-21 13:48:48 +08:00
README.md Rewrite the pf parser and rework the grammar (#6410) 2026-05-29 03:58:58 +08:00
serialize_functions.c Fix the missing release of RZ_OWN parameters (#5641) 2025-12-21 13:48:48 +08:00
serialize_types.c Convert %ll format specifiers to PFMT64 (#6267) 2026-04-22 05:57:04 +08:00
type.c Fix memory leaks related to DWARF (#6245) 2026-04-17 17:33:11 +00:00
typeclass.c Add HtSP, HtSS, HtSU, SetS (#4415) 2024-04-16 20:27:48 +08:00

Type database and pf format engine

This library owns Rizin's representation of C-style types (structs, unions, enums, typedefs, callables) and the pf print-format language for decoding raw bytes through that type information.

Public headers

Header Surface
<rz_type.h> RzTypeDB, base types, callables, format storage
<rz_pf.h> RzPfFormat, RzPfValue, RzPfCtx, parser/reader/renderer

<rz_pf.h> is included transitively from <rz_type.h>, so callers that already include the latter need not include the former explicitly.

File map

base.c                  RzBaseType (struct/union/enum/typedef) lifecycle
format.c                Format-string codegen from RzType; named-format storage
function.c              RzCallable definitions (function prototypes)
helpers.c               Type comparison, size, attribute helpers
path.c                  Path-style access into nested types
serialize_*.c           Project (de)serialization of types and functions
parser/                 Tree-sitter based C type parser

pf/                     The pf (print-format) engine. All sources live
                        under this directory; only format.c stays at the
                        librz/type/ top level.
pf/pf_parser.c          Parse driver, type-spec dispatcher, reader core,
                        parsing context, public utility surface
pf/pf_parser.h          Internal alias header -- includes <rz_pf.h>, plus
                        helpers shared with pf_render.c (is_string_type,
                        is_raw_type, endian_str, pf_vasprintf) as inlines
pf/pf_internal.h        Cross-TU glue: PF_DIAG diagnostic macro, the
                        shared ReadState, and declarations of every
                        function that crosses a pf file boundary
pf/pf_parser_string.c   String/encoding spec parsing + reading (z / s / Z)
pf/pf_parser_bitfield.c Inline + typed bitfield parsing (B...)
pf/pf_parser_bitvec.c   Bitvector parsing + reading (v(N))
pf/pf_parser_array.c    Array-count resolution ([N] / [@name])
pf/pf_parser_struct.c   Nested struct / union reading (?)
pf/pf_parser_time.c     Timestamp wire-format decoders
                        (filetime, dos, hfs, oletime, webkit, cocoa, ...)
pf/pf_parser_time.h     Private API between pf_parser.c and pf_parser_time.c
pf/pf_parser_tlv.c      TLV (Tag-Length-Value) record parsing and dispatch
pf/pf_render.c          Shared render helpers (pf_field_matches,
                        pf_scalar_text, pf_render_guid) and the
                        rz_pf_render() mode dispatcher
pf/pf_render.h          Cross-TU glue for the renderers: the RenderCtx
                        record and shared-helper / per-mode declarations
pf/pf_render_text.c     Text + quiet renderers
pf/pf_render_json.c     JSON renderer (+ rz_pf_render_json entry point)
pf/pf_render_cstruct.c  C-struct renderer
pf/pf_render_dot.c      Graphviz DOT renderer
pf/pf_render_sd.c       RzStructuredData renderer (rz_pf_render_sd)

pf architecture

The pf format engine is a three-stage pipeline. Each stage is a public API entry point so callers can mix and match:

   parse                read                       render
  +--------+         +------------------+         +---------------------+
  | source |  -->    | format -> values |  -->    | values -> string    |
  | string |         | (RzPfFormat,     |         | (text / json /      |
  |        |         |  RzPfValue[])    |         |  cstruct / quiet /  |
  +--------+         +------------------+         |  dot / write)       |
                                                  +---------------------+
Stage Entry point Inputs Output
parse rz_pf_parse format string RzPfFormat *
read rz_pf_read format, buffer, base addr, ctx RzPfValue *
render rz_pf_render values, count, mode, opts char *
all-in-one rz_pf_format source + buf + ctx + mode + opts char *

parse

The parser is a single-pass recursive-descent walker over the format string. For each token it decides the field type and any per-field metadata (endianness, array count, encoding, timestamp format, GUID layout, TLV spec, etc.). The token shape is the source of truth -- there is no separate lexer.

Diagnostics are recorded both on the RzPfFormat::errors[] array (with positioned messages) and on RZ_LOG_WARN (for backward compatibility). The parser uses two file-scoped pointers (g_current_fmt, g_current_src) to let leaf helpers emit positioned diagnostics without threading a context pointer through every signature; these are set on entry and restored on exit so the parser is safe to call recursively from read_nested_struct.

TLV specs (V(t=...,l=...,e=...,h=...,d=...)) and TLV value reads live in pf_parser_tlv.c. The two TUs cross-call through a small set of internal helpers (pf_emit_error, pf_current_fmt, pf_current_src, pf_parse_tlv_spec, pf_tlv_read) declared extern in pf_parser_tlv.c and defined in pf_parser.c.

read

The reader walks the parsed format over a byte buffer and produces one RzPfValue per top-level field, with nested struct children attached. The read pass uses a per-instance ReadState:

typedef struct {
    int bit_cursor;             /* 0..7 for :N bit fields */
    const RzPfValue *siblings;  /* for [@name] lookups */
    int n_siblings;
} ReadState;

The bit cursor is scoped to a single top-level repetition and a single nested-struct instance. When a non-bit field is read while bit_cursor != 0, the cursor snaps to the next byte boundary; this is the "snap-flush" rule.

Bitvector fields (v(N)) intentionally bypass the bit cursor: each v(N) reads exactly ceil(N/8) whole bytes and unpacks them into N individual 0/1 scalars. They do not pack with neighbouring :N fields, and reading a v(N) field next to a :N field will flush any partially-consumed bit cursor first. The bit-order knob (lsb vs msb) governs the per-byte unpacking, not the byte order itself.

[@name] length-by-reference works by scanning siblings[0..n_siblings) for an earlier field with the matching name. Lookups never cross struct boundaries.

Pointer dereference is delegated to the caller via RzPfCtx::read_at so the type subsystem stays I/O-agnostic; the disasm and core printers forward to rz_io_nread_at.

render

Rendering is a separate pass over the RzPfValue[]. Most modes produce a string; WRITE is handled by the bridge in librz/core rather than rz_pf_render, and the structured-data renderer returns a tree rather than text (see below):

  • text -- <offset> <name> : [endian] <value> one line per field. Optionally colorised via RzPfRenderOpts::palette; the palette holds ANSI-escape strings indexed by role (offset / name / endian / hex_literal / label / reset).
  • json -- JSON array (via PJ), with nested struct children under "fields".
  • cstruct -- C struct[ <name>] { ... } mirror with decoded values in comments. The format name (when invoked as pfc <name>) follows the struct keyword; anonymous formats render as struct { ... }.
  • quiet -- value-only, no offsets or names.
  • dot -- Graphviz digraph record node with four column-aligned rows (offset / type-glyph / name / value), one column per visible field. The graph label is taken from RzPfRenderOpts::graph_label.
  • structured data -- not a string mode: rz_pf_render_sd() returns an RzStructuredData tree (the generic key/value document model shared with rz_bin, ASN.1 and PKCS#7), which the caller can serialise to JSON or YAML or walk with the generic iterator.

RzPfRenderOpts additionally holds an optional field_filter (skip fields whose name does not match) used by pf.<name>.<field> selectors.

The renderer dispatcher is rz_pf_render() in pf_render.c, which fans out to one translation unit per mode (pf_render_text.c, pf_render_json.c, pf_render_cstruct.c, pf_render_dot.c, pf_render_sd.c). pf_render.c keeps only the dispatcher and the helpers shared across modes (pf_field_matches, pf_scalar_text, pf_render_guid); pf_render.h declares those plus the shared RenderCtx record and the per-mode entry points.

Typedb integration

Named formats are stored in RzTypeDB::formats (HtSS). rz_pf_resolve_name is the canonical resolver and the only function inside the type subsystem that callers should use to look up a format by name. The TLV dispatch table uses a tlv.<name>.<hex-tag> key convention in the same hash.

When a format references a typename via ?(Name) / E (Name) f / B (Name) f, the reader consults typedb->types (for enums and bitfields) or recursively rz_pf_resolve_name + rz_pf_parse (for nested struct formats). Recursion is bounded by RzPfCtx::max_depth (default 32) to catch self-referential structs cleanly.

Error reporting

Each RzPfError carries:

  • severity: RZ_PF_ERR_WARN (recoverable) or RZ_PF_ERR_ERROR (structural problem)
  • category: SYNTAX, SEMANTIC, RANGE, DEPRECATED, DATA, DEPTH
  • pos: 0-based column into the source format string
  • message: human-readable diagnostic

rz_pf_format_errors_to_string() renders the array with caret-position lines pointing at the offending column.

Reference

See doc/pf.md for the user-facing pf DSL reference.