#2679·ratatui

Paragraph with Wrap panics with "index outside of buffer" on double-width graphemes

Author: jchultarskyCreated Jul 28, 2026Updated Jul 28, 2026

Description

Paragraph::wrap can emit a wrapped line wider than the area it was given, and render_line writes it without bounds-checking x. The result is a panic:

index outside of buffer: the area is Rect { x: 0, y: 0, width: 2, height: 11 } but index is (2, 1)

The widget is rendered entirely inside the frame, so this is distinct from #1667, and it is a horizontal overrun rather than the vertical one in #1375.

Two inputs reach it, both involving grapheme widths that do not divide evenly into the remaining space on a row:

  1. A double-width glyph inside a word too long for a two-cell area. "ab" is enough.
  2. A leading combining mark — a grapheme cluster with no base character. This one is not confined to narrow areas; the example below is 8 cells wide, and with a mixed-width alphabet I have seen it at widths 3, 4, 5, 6, 8 and 10.

In both cases the panic index is (width, 1) — the second wrapped line. That detail matters for anyone else hitting this: a one-row area never reaches the faulty row, so the crash looks intermittent and height-dependent.

To Reproduce

ratatui = "0.30.2", no other dependencies:

use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::widgets::{Paragraph, Wrap};

fn draw(text: &str, width: u16, height: u16) {
    let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
    terminal
        .draw(|frame| {
            frame.render_widget(
                Paragraph::new(text).wrap(Wrap { trim: false }),
                frame.area(),
            );
        })
        .unwrap();
}

#[test]
fn wide_glyph_in_a_two_cell_area() {
    draw("a\\\u{1F31E}b", 2, 11); // panics: index is (2, 1)
}

#[test]
fn leading_combining_mark() {
    draw("\\\u{0301} \\\u{1F31E}\\\u{65E5}\\\u{65E5}\\\u{65E5}\\\u{1F31E}\\\u{1F31E}a", 8, 11); // panics: index is (8, 1)
}

// Controls, both of which pass — the same text without the leading mark, and
// a plain ASCII word at width 2.
#[test]
fn control_without_the_leading_mark() {
    draw(" \\\u{1F31E}\\\u{65E5}\\\u{65E5}\\\u{65E5}\\\u{1F31E}\\\u{1F31E}a", 8, 11);
}

#[test]
fn control_ascii_at_width_two() {
    draw("abc", 2, 11);
}

Wrap { trim: true } behaves the same. Reproduced on macOS with ratatui 0.30.2 / ratatui-widgets 0.3.2 / ratatui-core 0.1.2.

Expected behavior

Wrapping text should not panic. A grapheme that cannot fit the remaining cells should move to the next row, and one wider than the whole row should either overrun by itself or be dropped — but the buffer write should not go out of bounds either way.

Notes towards a fix

render_line (ratatui-widgets/src/paragraph.rs) advances x by each grapheme's cell_width() and indexes the buffer unconditionally:

let position = Position::new(area.left() + x, area.top() + y);
buf[position].set_symbol(symbol).set_style(*style);
x += width;

It relies on WordWrapper never yielding a line wider than area.width, and that invariant is what breaks. So there are arguably two changes: the accounting in WordWrapper for graphemes that straddle the row boundary, and a defensive if x + width > area.width { break } in render_line — every other widget clamps, and a wrapping bug should be a misdraw rather than a crash.

Happy to open a PR if you'd like a particular one of those.

Environment

  • ratatui 0.30.2 (ratatui-widgets 0.3.2, ratatui-core 0.1.2)
  • rustc stable, macOS (backend-independent — reproduced with TestBackend)

Found while hardening mirador against untrusted input: a note containing an emoji crashed the dashboard.