With bin.demangle=true the call/jump operand is replaced by the full
demangled symbol name and then colorized token-by-token before being
filtered. For long C++ symbols (e.g. libc++ STL names) the colorized
operand easily exceeds the 1024-byte operand buffer (ds->str): in
truecolor each token is wrapped in a ~19-byte "\x1b[38;2;R;G;Bm" escape,
so a ~110-char name grows past 1700 bytes.
The generic filter() copied that operand with
strncpy(str, data, len);
which has two problems when strlen(data) >= len:
- it does not NUL-terminate, so reading str ran past the buffer into
the adjacent strsub[] buffer, printing stale bytes (the "call 0x..."
bleed); and
- the byte-bounded cut can land in the middle of a color escape,
leaving a partial "\x1b[38;2;.." that, followed by the bled bytes,
forms a complete but bogus "\x1b[..c" sequence. Terminals read that
as a Device-Attributes query and reply with e.g. "62;4c", which is
the garbage the reporter saw at the prompt.
This only triggers with color enabled and a symbol long enough that its
colorized form overflows the buffer, which is why it showed up on some
x86 files only and had no portable reproducer (no-color = 149 bytes,
16-color = 763, 256-color = 1170, truecolor = 1713 for the sample
symbol; the threshold is 1024).
Add test/unit/test_parse.c driving rz_parse_filter() with over-long
colored operands (a deterministic case whose cut lands inside an escape,
and a realistic libc++-style colorized operand) plus a short-operand
passthrough case. The two overflow tests fail on the old strncpy and
pass with the fix.
Co-authored-by: Anton Kochkov <anton.kochkov@gmail.com>
159 lines
6.1 KiB
C
159 lines
6.1 KiB
C
// SPDX-FileCopyrightText: 2026 Anton Kochkov <anton.kochkov@gmail.com>
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
#include <rz_parse.h>
|
|
#include <rz_util.h>
|
|
#include <limits.h>
|
|
#include "minunit.h"
|
|
|
|
// Mimic rz_print_colorize_asm_str(): wrap a single token in its own truecolor
|
|
// SGR escape, exactly as the disassembler does before the operand is handed to
|
|
// rz_parse_filter(). A demangled C++ symbol tokenizes into dozens of such
|
|
// tokens, so the colorized operand balloons well past the fixed operand buffer.
|
|
static void append_colored_token(RzStrBuf *sb, const char *tok) {
|
|
rz_strbuf_appendf(sb, "\x1b[38;2;204;204;204m%s\x1b[0m", tok);
|
|
}
|
|
|
|
// Return true if `s` (scanned up to `max` bytes, stopping at NUL) contains a CSI
|
|
// escape "\x1b[" whose parameter/intermediate bytes are not followed by a final
|
|
// byte (0x40-0x7e), i.e. a truncated/garbled escape. Emitting such a fragment is
|
|
// what made the terminal answer with a Device-Attributes reply in issue #3831.
|
|
static bool has_unterminated_csi(const char *s, size_t max) {
|
|
for (size_t i = 0; i < max && s[i]; i++) {
|
|
if ((unsigned char)s[i] != 0x1b) {
|
|
continue;
|
|
}
|
|
if (i + 1 >= max || s[i + 1] != '[') {
|
|
return true; // lone/unknown ESC
|
|
}
|
|
size_t j = i + 2;
|
|
// CSI parameter (0x30-0x3f) and intermediate (0x20-0x2f) bytes
|
|
while (j < max && s[j] && (unsigned char)s[j] >= 0x20 && (unsigned char)s[j] <= 0x3f) {
|
|
j++;
|
|
}
|
|
if (j >= max || !s[j] || !((unsigned char)s[j] >= 0x40 && (unsigned char)s[j] <= 0x7e)) {
|
|
return true; // no final byte before end -> truncated escape
|
|
}
|
|
i = j;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Regression for https://github.com/rizinorg/rizin/issues/3831
|
|
// A colored operand longer than the destination buffer must be truncated
|
|
// without splitting an ANSI escape and must stay NUL-terminated inside the
|
|
// buffer. The plain strncpy() used before neither terminated the buffer (so the
|
|
// operand bled into the adjacent strsub[] buffer) nor respected escape
|
|
// boundaries (so the cut could land inside "\x1b[38;2;.." and, followed by the
|
|
// bled bytes, form a stray "\x1b[..c" Device-Attributes query).
|
|
bool test_rz_parse_filter_ansi_truncation_no_partial_escape(void) {
|
|
RzParse *p = rz_parse_new();
|
|
mu_assert_notnull(p, "rz_parse_new");
|
|
// No analysis/flags are bound in this isolated test; keep the substitution
|
|
// branches from consulting them so only the final copy path is exercised.
|
|
p->minval = INT_MAX;
|
|
|
|
const int len = 1024; // == sizeof(ds->str) in librz/core/disasm.c
|
|
// 1020 filler bytes followed by a fresh color escape, so that the cut at
|
|
// len-1 deterministically lands inside the "\x1b[38;2;.." parameters.
|
|
RzStrBuf *sb = rz_strbuf_new("");
|
|
for (int i = 0; i < 1020; i++) {
|
|
rz_strbuf_append(sb, "A");
|
|
}
|
|
rz_strbuf_append(sb, "\x1b[38;2;204;204;204mTAIL\x1b[0m");
|
|
char *data = rz_strbuf_drain(sb);
|
|
mu_assert_true(strlen(data) > (size_t)len, "precondition: colored operand overflows the buffer");
|
|
|
|
char *str = malloc(len);
|
|
mu_assert_notnull(str, "malloc");
|
|
memset(str, 0xff, len); // no stale NUL, so a missing terminator is detectable
|
|
|
|
rz_parse_filter(p, 0x1000, NULL, NULL, data, str, len, false);
|
|
|
|
mu_assert_notnull(memchr(str, 0, len), "filtered string must be NUL-terminated within the buffer");
|
|
mu_assert_false(has_unterminated_csi(str, len), "filtered string must not contain a partial ANSI escape");
|
|
mu_assert_true(strlen(str) <= (size_t)(len - 1), "length stays within the buffer");
|
|
mu_assert_memeq((const ut8 *)str, (const ut8 *)data, strlen(str), "result is a verbatim prefix of the input (no bleed)");
|
|
|
|
free(str);
|
|
free(data);
|
|
rz_parse_free(p);
|
|
mu_end;
|
|
}
|
|
|
|
// Same regression, driven by a realistic colorized libc++ demangled operand
|
|
// (the kind `pdf` produces with bin.demangle=true and truecolor).
|
|
bool test_rz_parse_filter_ansi_truncation_realistic_symbol(void) {
|
|
RzParse *p = rz_parse_new();
|
|
mu_assert_notnull(p, "rz_parse_new");
|
|
p->minval = INT_MAX;
|
|
|
|
const int len = 1024;
|
|
static const char *toks[] = {
|
|
"call", " ", "std", "::", "__1", "::", "vector", "<", "std", "::",
|
|
"__1", "::", "basic_string", "<", "char", ",", " ", "std", "::", "__1",
|
|
"::", "char_traits", "<", "char", ">", ">", ">", "::", "method", "(",
|
|
"std", "::", "__1", "::", "allocator", "<", "char", ">", ")"
|
|
};
|
|
RzStrBuf *sb = rz_strbuf_new("");
|
|
while (rz_strbuf_length(sb) <= (size_t)(len + 64)) {
|
|
for (size_t i = 0; i < RZ_ARRAY_SIZE(toks); i++) {
|
|
append_colored_token(sb, toks[i]);
|
|
}
|
|
}
|
|
char *data = rz_strbuf_drain(sb);
|
|
mu_assert_true(strlen(data) > (size_t)len, "precondition: colored operand overflows the buffer");
|
|
|
|
char *str = malloc(len);
|
|
mu_assert_notnull(str, "malloc");
|
|
memset(str, 0xff, len);
|
|
|
|
rz_parse_filter(p, 0x1000, NULL, NULL, data, str, len, false);
|
|
|
|
mu_assert_notnull(memchr(str, 0, len), "filtered string must be NUL-terminated within the buffer");
|
|
mu_assert_false(has_unterminated_csi(str, len), "filtered string must not contain a partial ANSI escape");
|
|
mu_assert_memeq((const ut8 *)str, (const ut8 *)data, strlen(str), "result is a verbatim prefix of the input (no bleed)");
|
|
|
|
free(str);
|
|
free(data);
|
|
rz_parse_free(p);
|
|
mu_end;
|
|
}
|
|
|
|
// A colored operand that fits within the buffer must be copied through
|
|
// unchanged (and NUL-terminated): the fix must not alter the common case.
|
|
bool test_rz_parse_filter_ansi_short_passthrough(void) {
|
|
RzParse *p = rz_parse_new();
|
|
mu_assert_notnull(p, "rz_parse_new");
|
|
p->minval = INT_MAX;
|
|
|
|
const int len = 1024;
|
|
RzStrBuf *sb = rz_strbuf_new("");
|
|
append_colored_token(sb, "call");
|
|
append_colored_token(sb, " ");
|
|
append_colored_token(sb, "sym.imp.puts");
|
|
char *data = rz_strbuf_drain(sb);
|
|
mu_assert_true(strlen(data) < (size_t)len, "precondition: colored operand fits in the buffer");
|
|
|
|
char *str = malloc(len);
|
|
mu_assert_notnull(str, "malloc");
|
|
memset(str, 0xff, len);
|
|
|
|
rz_parse_filter(p, 0x1000, NULL, NULL, data, str, len, false);
|
|
|
|
mu_assert_streq(str, data, "short colored operand must be copied unchanged");
|
|
|
|
free(str);
|
|
free(data);
|
|
rz_parse_free(p);
|
|
mu_end;
|
|
}
|
|
|
|
int all_tests() {
|
|
mu_run_test(test_rz_parse_filter_ansi_truncation_no_partial_escape);
|
|
mu_run_test(test_rz_parse_filter_ansi_truncation_realistic_symbol);
|
|
mu_run_test(test_rz_parse_filter_ansi_short_passthrough);
|
|
return tests_passed != tests_run;
|
|
}
|
|
|
|
mu_main(all_tests)
|