Skip to content

rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to _ or : - #160232

Merged
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
lazureykis:fix/rustdoc-escape-char-boundary
Jul 31, 2026
Merged

rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to _ or :#160232
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
lazureykis:fix/rustdoc-escape-char-boundary

Conversation

@lazureykis

@lazureykis lazureykis commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #160231

EscapeBodyTextWithWbr iterates text.grapheme_indices(true), so i is the start of a grapheme cluster, but the _/: word-break arm sliced at i + 1 — hard-coding the assumption that a cluster containing _ or : is exactly one byte long.

UAX#29 GB9b joins a Prepend-class character (U+0600U+0605, U+0D4E, U+111C2, …) with the character that follows it, so a cluster can start with a multi-byte character and still contain _. i + 1 then lands inside that character and str indexing panics. U+0D4E is XID_Continue, so this is reachable from an ordinary item name that rustc accepts:

pub struct abcൎ_defgh;

rustdoc ICEs on that with end byte index 4 is not a char boundary; it is inside 'ൎ' (bytes 3..6 of string), which means cargo doc cannot document the crate at all.

Break after the whole cluster (i + s.len()) instead. The adjacent CamelCase arm already slices at i, which is always a cluster boundary, so it needed no change.

The i + 1 dates to 3bf8bcf (which added the : arm) and was extended to _ by ac303df, both in #126247.

This also fixes a smaller, non-panicking case: when a combining mark trails the _ (first_◌̀second), the old code inserted the <wbr> between _ and its combining mark, splitting a grapheme cluster.

Tests: unit cases in src/librustdoc/html/escape/tests.rs covering a Prepend+_ cluster, a Prepend+: cluster, and the trailing-combining-mark case; plus an end-to-end regression test in tests/rustdoc-html/ so the ICE itself stays fixed.

The existing tests missed this because they only cover Extend-class clusters, which join backwards onto an ASCII base character and so keep i + 1 on a boundary (E("ṼẽçÑñéå"), E("V\u{0300}e\u{0300}…")). Prepend is the one class that joins forwards. The property test escape_body_text_with_wbr_makes_sense can't reach it either — its alphabet is [b'a', b'A', b'_'].

r? rustdoc

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output. labels Jul 30, 2026
@rustbot

rustbot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust Project is excited to review your changes, and you should hear from @notriddle (or someone else) some time within the next two weeks.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue
Why was this reviewer chosen?

The reviewer was selected based on:

  • rustdoc expanded to 8 candidates
  • Random selection from GuillaumeGomez, lolbinarycat, notriddle

… `_`

`EscapeBodyTextWithWbr` iterates `text.grapheme_indices(true)`, so `i` is the
start of a grapheme cluster, but the `_`/`:` word-break arm sliced at `i + 1` --
assuming a cluster containing `_` or `:` is exactly one byte long.

UAX#29 GB9b joins a `Prepend`-class character (U+0600-U+0605, U+0D4E, U+111C2,
...) with the character that follows it, so a cluster can start with a
multi-byte character and still contain `_`. `i + 1` then lands inside that
character and `str` indexing panics. U+0D4E is `XID_Continue`, so this is
reachable from an item name rustc accepts, e.g. `pub struct abc<U+0D4E>_defgh;`,
and rustdoc ICEs instead of documenting the crate.

Break after the whole cluster (`i + s.len()`) instead. The adjacent CamelCase
arm already slices at `i`, which is always a cluster boundary, so it is
unaffected.

This also stops the `<wbr>` from being inserted between `_` and a combining
mark that trails it, which split a grapheme cluster without panicking.
@lazureykis
lazureykis force-pushed the fix/rustdoc-escape-char-boundary branch from 75c7214 to 54941be Compare July 30, 2026 19:23
@lazureykis
lazureykis marked this pull request as ready for review July 30, 2026 19:34
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 30, 2026
@notriddle

Copy link
Copy Markdown
Contributor

@bors r+

@rust-bors

rust-bors Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 54941be has been approved by notriddle

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 30, 2026
rust-bors Bot pushed a commit that referenced this pull request Jul 31, 2026
…uwer

Rollup of 25 pull requests

Successful merges:

 - #160204 (Sync from portable simd 2026 07 30)
 - #138230 (Add `raw_borrows_via_references` lint)
 - #158057 (Don't escape U+FF9E and U+FF9F in `escape_debug_ext`)
 - #160015 (refactor(mir-transform): Merge `can_be_overridden`, `is_required` and `is_enabled` into one)
 - #160031 (std: make positioned I/O unsupported on VxWorks)
 - #160125 (Fix typing mode handling in transmute checks and rustc_dump_layout)
 - #160152 (Create on-demand CI job for testing EC2 instances)
 - #160232 (rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to `_` or `:`)
 - #159214 (std: improve the documentation of the random feature)
 - #159818 (Resolve vars before calling `unnormalized_obligations`)
 - #159955 (Stop using higher-order macros to declare arenas)
 - #159958 (Fix avoid cycle for self referential return type notation)
 - #160040 (Split function parsing out of `item.rs` to a new module.)
 - #160044 (Add regression tests for fixed dead-code issues)
 - #160144 (renovate: group lockfiles PRs)
 - #160149 (Fix Windows on Arm PAC default)
 - #160164 (Derive `GenericTypeVisitable` for `RegionConstraint`)
 - #160175 (Try to recover less from incorrectly parsed const arg)
 - #160177 (A few more "predicate"-to-"clause" renamings)
 - #160181 (Mark `Tuple` and `FnPtr` traits `#[fundamental]`)
 - #160192 (Fix ICE for parsing issue with a closing brace)
 - #160209 (bootstrap: Remove method `Subcommand::kind`)
 - #160221 (Remove `Copy` supertrait from `VaList`)
 - #160223 (interpret: rename validate_operand → validate_place)
 - #160234 (Always use short ty path for call with missing arguments suggestion)
rust-bors Bot pushed a commit that referenced this pull request Jul 31, 2026
…uwer

Rollup of 25 pull requests

Successful merges:

 - #160204 (Sync from portable simd 2026 07 30)
 - #138230 (Add `raw_borrows_via_references` lint)
 - #158057 (Don't escape U+FF9E and U+FF9F in `escape_debug_ext`)
 - #160015 (refactor(mir-transform): Merge `can_be_overridden`, `is_required` and `is_enabled` into one)
 - #160031 (std: make positioned I/O unsupported on VxWorks)
 - #160125 (Fix typing mode handling in transmute checks and rustc_dump_layout)
 - #160152 (Create on-demand CI job for testing EC2 instances)
 - #160232 (rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to `_` or `:`)
 - #159214 (std: improve the documentation of the random feature)
 - #159818 (Resolve vars before calling `unnormalized_obligations`)
 - #159955 (Stop using higher-order macros to declare arenas)
 - #159958 (Fix avoid cycle for self referential return type notation)
 - #160040 (Split function parsing out of `item.rs` to a new module.)
 - #160044 (Add regression tests for fixed dead-code issues)
 - #160144 (renovate: group lockfiles PRs)
 - #160149 (Fix Windows on Arm PAC default)
 - #160164 (Derive `GenericTypeVisitable` for `RegionConstraint`)
 - #160175 (Try to recover less from incorrectly parsed const arg)
 - #160177 (A few more "predicate"-to-"clause" renamings)
 - #160181 (Mark `Tuple` and `FnPtr` traits `#[fundamental]`)
 - #160192 (Fix ICE for parsing issue with a closing brace)
 - #160209 (bootstrap: Remove method `Subcommand::kind`)
 - #160221 (Remove `Copy` supertrait from `VaList`)
 - #160223 (interpret: rename validate_operand → validate_place)
 - #160234 (Always use short ty path for call with missing arguments suggestion)
@rust-bors
rust-bors Bot merged commit 8adc6f5 into rust-lang:main Jul 31, 2026
13 checks passed
@rustbot rustbot added this to the 1.99.0 milestone Jul 31, 2026
rust-timer added a commit that referenced this pull request Jul 31, 2026
Rollup merge of #160232 - lazureykis:fix/rustdoc-escape-char-boundary, r=notriddle

rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to `_` or `:`

Fixes #160231

`EscapeBodyTextWithWbr` iterates `text.grapheme_indices(true)`, so `i` is the start of a grapheme cluster, but the `_`/`:` word-break arm sliced at `i + 1` — hard-coding the assumption that a cluster containing `_` or `:` is exactly one byte long.

UAX#29 GB9b joins a `Prepend`-class character (`U+0600`–`U+0605`, `U+0D4E`, `U+111C2`, …) with the character that follows it, so a cluster can start with a multi-byte character and still contain `_`. `i + 1` then lands inside that character and `str` indexing panics. `U+0D4E` is `XID_Continue`, so this is reachable from an ordinary item name that rustc accepts:

```rust
pub struct abcൎ_defgh;
```

rustdoc ICEs on that with `end byte index 4 is not a char boundary; it is inside 'ൎ' (bytes 3..6 of string)`, which means `cargo doc` cannot document the crate at all.

Break after the whole cluster (`i + s.len()`) instead. The adjacent CamelCase arm already slices at `i`, which is always a cluster boundary, so it needed no change.

The `i + 1` dates to 3bf8bcf (which added the `:` arm) and was extended to `_` by ac303df, both in #126247.

This also fixes a smaller, non-panicking case: when a combining mark trails the `_` (`first_◌̀second`), the old code inserted the `<wbr>` between `_` and its combining mark, splitting a grapheme cluster.

Tests: unit cases in `src/librustdoc/html/escape/tests.rs` covering a `Prepend`+`_` cluster, a `Prepend`+`:` cluster, and the trailing-combining-mark case; plus an end-to-end regression test in `tests/rustdoc-html/` so the ICE itself stays fixed.

The existing tests missed this because they only cover `Extend`-class clusters, which join backwards onto an ASCII base character and so keep `i + 1` on a boundary (`E("ṼẽçÑñéå")`, `E("V\u{0300}e\u{0300}…")`). `Prepend` is the one class that joins forwards. The property test `escape_body_text_with_wbr_makes_sense` can't reach it either — its alphabet is `[b'a', b'A', b'_']`.

r? rustdoc
RalfJung pushed a commit to RalfJung/miri that referenced this pull request Jul 31, 2026
…uwer

Rollup of 25 pull requests

Successful merges:

 - rust-lang/rust#160204 (Sync from portable simd 2026 07 30)
 - rust-lang/rust#138230 (Add `raw_borrows_via_references` lint)
 - rust-lang/rust#158057 (Don't escape U+FF9E and U+FF9F in `escape_debug_ext`)
 - rust-lang/rust#160015 (refactor(mir-transform): Merge `can_be_overridden`, `is_required` and `is_enabled` into one)
 - rust-lang/rust#160031 (std: make positioned I/O unsupported on VxWorks)
 - rust-lang/rust#160125 (Fix typing mode handling in transmute checks and rustc_dump_layout)
 - rust-lang/rust#160152 (Create on-demand CI job for testing EC2 instances)
 - rust-lang/rust#160232 (rustdoc: fix ICE when a grapheme cluster joins a Prepend-class character to `_` or `:`)
 - rust-lang/rust#159214 (std: improve the documentation of the random feature)
 - rust-lang/rust#159818 (Resolve vars before calling `unnormalized_obligations`)
 - rust-lang/rust#159955 (Stop using higher-order macros to declare arenas)
 - rust-lang/rust#159958 (Fix avoid cycle for self referential return type notation)
 - rust-lang/rust#160040 (Split function parsing out of `item.rs` to a new module.)
 - rust-lang/rust#160044 (Add regression tests for fixed dead-code issues)
 - rust-lang/rust#160144 (renovate: group lockfiles PRs)
 - rust-lang/rust#160149 (Fix Windows on Arm PAC default)
 - rust-lang/rust#160164 (Derive `GenericTypeVisitable` for `RegionConstraint`)
 - rust-lang/rust#160175 (Try to recover less from incorrectly parsed const arg)
 - rust-lang/rust#160177 (A few more "predicate"-to-"clause" renamings)
 - rust-lang/rust#160181 (Mark `Tuple` and `FnPtr` traits `#[fundamental]`)
 - rust-lang/rust#160192 (Fix ICE for parsing issue with a closing brace)
 - rust-lang/rust#160209 (bootstrap: Remove method `Subcommand::kind`)
 - rust-lang/rust#160221 (Remove `Copy` supertrait from `VaList`)
 - rust-lang/rust#160223 (interpret: rename validate_operand → validate_place)
 - rust-lang/rust#160234 (Always use short ty path for call with missing arguments suggestion)
@lazureykis
lazureykis deleted the fix/rustdoc-escape-char-boundary branch July 31, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ICE]: rustdoc panics with "not a char boundary" on an item name that joins a Prepend-class character to _

3 participants