Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(text): fix out of bounds access triggered by zero-width graphemes #1001

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

andyyu2004
Copy link

Whilst indirectly fuzzing this library I found a panic in the span rendering logic. This PR provides a minimized reproduction and a small patch to avoid the panic.

Copy link

codecov bot commented Mar 27, 2024

Codecov Report

Attention: Patch coverage is 75.00000% with 2 lines in your changes are missing coverage. Please review.

Project coverage is 89.3%. Comparing base (742a5ea) to head (d82e211).

Files Patch % Lines
src/text/text.rs 71.4% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #1001     +/-   ##
=======================================
- Coverage   89.3%   89.3%   -0.1%     
=======================================
  Files         61      61             
  Lines      15298   15305      +7     
=======================================
+ Hits       13670   13675      +5     
- Misses      1628    1630      +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@EdJoPaTo EdJoPaTo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have only looked over this in the browser without experimenting with it in code so maybe I have false assumptions about the specifics here. I might have overlooked something obvious.

src/text/text.rs Outdated Show resolved Hide resolved
@@ -374,7 +374,7 @@ impl WidgetRef for Span<'_> {
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
if next_x > max_x || current_x >= max_x {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_x is the same or bigger than current_x so it’s somewhat pointless to check that the smaller one is ≥ while the bigger is only >.

I am not sure where the panic is happening but I assume it’s in the for loop over the range? Can that be written more clearly then which avoids the pre-check? Like when not possible to create the range, then skip it completely, otherwise loop over it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panic occurs on the statement immediately after the break. buf.get_mut(current_x, y). In particular, current_x == max_x and is out of bounds.

I don't think either check is pointless, suppose we have the current check next_x > max_x, then next_x = current_x = max_x = 1 does not break the loop (and panics).
If we keep only current_x >= max_x, then next_x = 100 && current_x = max_x = 1 does not break and that panics further down in the range loop.

Let me know if you have a better idea to fix this!

@andyyu2004 andyyu2004 force-pushed the zero-width-grapheme-span-panic-fix branch from 48d9093 to d82e211 Compare March 27, 2024 07:27
@joshka joshka mentioned this pull request Apr 10, 2024
4 tasks
@EdJoPaTo
Copy link
Member

Sorry for not looking into this sooner. The source code here is hard to read and a bit confusing. The variable names are also not that helpful to understand what is happening in the code.

I refactored this to easier to understand the logic in my opinion. This basically says "get graphemes while there is width remaining" now. Any thoughts on this?

 impl WidgetRef for Span<'_> {
     fn render_ref(&self, area: Rect, buf: &mut Buffer) {
         let area = area.intersection(buf.area);
-        let Rect {
-            x: mut current_x,
-            y,
-            ..
-        } = area;
-        let max_x = area.right();
-        for g in self.styled_graphemes(Style::default()) {
-            let symbol_width = g.symbol.width();
-            let next_x = current_x.saturating_add(symbol_width as u16);
-            if next_x > max_x {
-                break;
-            }
-            buf.get_mut(current_x, y)
-                .set_symbol(g.symbol)
-                .set_style(g.style);
+        let mut remaining_width = area.width;
+        let graphemes = self.styled_graphemes(Style::new()).map_while(|grapheme| {
+            let symbol_width = grapheme.symbol.width() as u16;
+            remaining_width = remaining_width.checked_sub(symbol_width)?;
+            Some((grapheme, symbol_width))
+        });
+        let Rect { mut x, y, .. } = area;
+        for (grapheme, symbol_width) in graphemes {
+            buf.get_mut(x, y)
+                .set_symbol(grapheme.symbol)
+                .set_style(grapheme.style);

             // multi-width graphemes must clear the cells of characters that are hidden by the
             // grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
             // overwritten.
-            for i in (current_x + 1)..next_x {
+            let x_after_symbol = x.saturating_add(symbol_width);
+            for i in (x + 1)..x_after_symbol {
                 buf.get_mut(i, y).reset();
                 // it may seem odd that the style of the hidden cells are not set to the style of
                 // the grapheme, but this is how the existing buffer.set_span() method works.
                 // buf.get_mut(i, y).set_style(g.style);
             }
-            current_x = next_x;
+            x = x_after_symbol;
         }
     }
 }

(Will result in a merge conflict with #1047. I somewhat use #1047 already in this before-diff. The new code works with and without #1047 and is way easier to understand in my opinion.)

@EdJoPaTo
Copy link
Member

EdJoPaTo commented Apr 17, 2024

There is also Buffer::set_span. There should be a single place to implement this. Either here or there. Then there is a single place to fix a bug instead of multiple ones.

Line has a similar problem. There is Buffer::set_line and impl WidgetRef for Line<'_>.

Also Buffer::set_stringn has a similar logic… Too many places with similar code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants