[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)
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
- Lĩnh vực
- backend-api-design, security
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"), commit0a5f9c956cb62b371ab680868d08c82ae0d49201(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 andnis used as(size_t) n, i.e. a near-SIZE_MAXbound — the same input then drives an out-of-bounds read in the DFA scan instead of aborting.
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
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
-
area:gates bug good first issue
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
InauguralSystems/EigenScript#1782 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
intel/intel-device-plugins-for-kubernetes#2384 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
zephyrproject-rtos/zephyr#122261 ·
Maintainer thường phản hồi trong vòng 3 ngày