Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

[Bug]pm_errors_format() passes a negative length to pm_parser_encoding_char_width(), tripping assert(n >= 0) in pm_encoding_utf_8_char_width() (CWE-617 / CWE-191)

Đã đóng
#4,251 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
48/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
c

Hướng nghiên cứu

Start with pm_rich_errors_format() in src/errors_format.c, especially the character-width calls around lines 318, 334, and 340, then trace the cursor and remaining-length assumptions into pm_parser_encoding_char_width() and the UTF-8 helper in src/encoding.c. Reproduce with the seven-byte input /(\0/=~ using the documented sanitizer build. Done means formatting diagnostics handles the malformed input without an assertion or out-of-bounds read; add a regression test for the case.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Hi,

While fuzzing prism, I found that formatting diagnostics for certain malformed input advances the error-column cursor past the end of the source and then calls pm_parser_encoding_char_width() with a negative remaining length, tripping assert(n >= 0) in pm_encoding_utf_8_char_width(). The input is 7 bytes and the abort is reached through normal parsing + error rendering, so any consumer that parses untrusted Ruby and formats the diagnostics aborts.

Summary

pm_rich_errors_format() walks each error line by character width, using the byte count still remaining from the current column to the end of the buffer:

/* src/errors_format.c:315-320 */
while (column < rich_error->column_start) {
    pm_buffer_append_byte(buffer, ' ');

    size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column));
    column += (char_width == 0 ? 1 : char_width);
}

If the error's column_start (or column_end) is past the end of the source, column is driven past parser->end, and the third argument parser->end - (start + column) becomes a negative ptrdiff_t. That value is forwarded to the active encoding's char_width callback, and the UTF-8 one asserts on it:

/* src/encoding.c:2365-2366 */
size_t
pm_encoding_utf_8_char_width(const uint8_t *b, ptrdiff_t n) {
    assert(n >= 0);
    ...
}
  • Affected versions: prism 1.9.0 (PRISM_VERSION "1.9.0"), commit 0a5f9c956cb62b371ab680868d08c82ae0d49201 (2026-10-09).
  • CWE: CWE-617 (Reachable Assertion) / CWE-191 (Integer Underflow — the remaining length goes negative).

Detail

Affected code

src/errors_format.c, pm_rich_errors_format() (:318, and the same pattern at :334, :340, :200). The column cursor is advanced with a per-character width taken from pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column)) without clamping the cursor to the buffer end. When start + column > parser->end, the remaining count underflows.

Root cause: the character-width helpers are contractually called with a non-negative remaining length (assert(n >= 0), and n is also used to compute maximum = (n > 4) ? 4 : (size_t) n, so a negative n would also be a huge size_t if the assert were compiled out — i.e. the same bug becomes an out-of-bounds read in a NDEBUG build). The error formatter can hand them a negative length because the error location's column is not clamped to the actual source length.

Trigger path
pm_serialize_parse_errors_format()   (src/prism.c:23559)
  -> pm_errors_format()              (src/errors_format.c:495)
       -> pm_rich_errors_format()    (src/errors_format.c:318)
            -> pm_parser_encoding_char_width()   (src/parser.c:78)
                 -> pm_encoding_utf_8_char_width()  (src/encoding.c:2366)  <-- assert(n >= 0)

Build the reproducer

git clone https://github.com/ruby/prism.git
cd prism
git checkout 0a5f9c956cb62b371ab680868d08c82ae0d49201
ruby templates/template.rb

Use the same driver.c as the companion report (prism-issue-1-file-keyword-null-serialize.md) — it calls pm_serialize_parse_errors_format() with format_type = (buf[0] % 3) + 1, which for this input selects PM_ERRORS_FORMAT_COLOR — and build:

clang -g -O1 -std=c99 -fsanitize=address,undefined -fno-sanitize-recover=all \
      -Iinclude -Isrc -I. driver.c src/*.c -lm -o repro

POC (tested on the unmodified source)

7 bytes: /(\0/=~. Rebuild it from hex:

printf '%s' '2f285c302f3d7e' | xxd -r -p > poc.rb
sha256sum poc.rb
# must print: fbaa0cf2ba6dffa17363d85360ffc48d6ad8f8fa8425147e8d051c3e72ddbdca
ls -l poc.rb     # 7 bytes
cat poc.rb       # /(\0/=~

Run:

./repro poc.rb

Trigger result

Verified on unmodified prism 1.9.0 (0a5f9c95) built as above:

prism: src/encoding.c:2366: size_t pm_encoding_utf_8_char_width(const uint8_t *, ptrdiff_t):
       Assertion `n >= 0' failed.
Program received signal SIGABRT, Aborted.
    #7 pm_encoding_utf_8_char_width   src/encoding.c:2366
    #8 pm_rich_errors_format          src/errors_format.c:318
    #9 pm_errors_format               src/errors_format.c:495
   #10 pm_serialize_parse_errors_format  src/prism.c:23559
   #11 LLVMFuzzerTestOneInput
  • Exit status: 134 (SIGABRT).
  • Deterministic: yes — the 7-byte file reproduces it on every run.
  • Note: in a NDEBUG (release) build the assert is compiled out and n is used as (size_t) n, i.e. a near-SIZE_MAX bound — the same input then drives an out-of-bounds read in the DFA scan instead of aborting.
Image

Impact

Error formatting runs whenever a parser consumer reports a syntax error on untrusted input; here a 7-byte source aborts the process (or, without asserts, becomes an out-of-bounds read). Any tool that parses untrusted Ruby and formats diagnostics (e.g. pm_serialize_parse_errors_format(), the error renderer used by tooling) is affected.

Suggested fix

Clamp the column cursor (and the remaining length) to the source end before calling the width helper, and/or make the width helper total (remove the n >= 0 precondition by returning 0 / 1 for non-positive n):

/* src/parser.c, or at each call site in errors_format.c */
static size_t
pm_parser_encoding_char_width_safe(pm_parser_t *parser, const uint8_t *cursor, ptrdiff_t remaining) {
    if (remaining <= 0) return 0;
    return parser->encoding->char_width(cursor, remaining);
}

and guard each start + column <= parser->end in pm_rich_errors_format(). Fixing pm_encoding_utf_8_char_width() (and siblings) to handle n <= 0 also removes the OOB read in release builds.

Ngôn ngữ chính
C
Star
1k
Fork
195
Merge trung bình
1 ngày 19 giờ
Pull request đã merge (30 ngày)
34

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue tương tự

Thêm issue về C

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.