Summary
rustc panics (ICE) inside the diagnostic renderer (annotate_snippets / StyledBuffer::replace) with slice index starts at N but ends at N-1 whenever it has to render a diagnostic whose underlined source line contains multibyte UTF-8 characters (e.g. CJK), and the JSON error format is used (--error-format=json --json=diagnostic-rendered-ansi, which is what Cargo/Clippy/rustdoc/cargo-nextest/rust-analyzer pass by default).
The panic is in the rendering of the rendered field of the JSON diagnostic, i.e. it happens after the warning/error/lint has already been produced. As a result, an ordinary, fixable diagnostic (e.g. unused_mut) turns into an opaque the compiler unexpectedly panicked and the actual diagnostic is never shown.
Version
rustc 1.95.0 (59807616e 2026-04-14)
host: x86_64-unknown-linux-gnu
--error-format=human / --error-format=short are not affected (those paths do not go through the annotate-snippets snippet renderer).
Panic message + backtrace (anonymized)
thread 'rustc' panicked at library/core/src/slice/index.rs:1031:55:
slice index starts at 17 but ends at 16
stack backtrace:
...
11: core::slice::index::slice_index_fail
12: <annotate_snippets::renderer::styled_buffer::StyledBuffer>::replace
13: annotate_snippets::renderer::render::render
14: <rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter>::emit_messages_default
15: <rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter as rustc_errors::emitter::Emitter>::emit_diagnostic
16: <rustc_errors::json::Diagnostic>::from_errors_diagnostic
17: <rustc_errors::json::JsonEmitter as rustc_errors::emitter::Emitter>::emit_diagnostic
18: <rustc_errors::DiagCtxtInner>::emit_diagnostic::{closure#3}
...
23: rustc_middle::lint::diag_lint_level::diag_lint_level_impl
...
31: rustc_interface::passes::analysis
...
error: the compiler unexpectedly panicked. This is a bug.
note: rustc 1.95.0 (59807616e 2026-04-14) running on x86_64-unknown-linux-gnu
note: compiler flags: -C embed-bitcode=no -C debuginfo=2
query stack during panic:
#0 [mir_borrowck] borrow-checking `some_function`
#1 [analysis] running analysis passes on crate `mycrate`
end of query stack
I observed the identical crash (same StyledBuffer::replace slice panic, with values such as 13 but ends at 12, 17 but ends at 16) from several different diagnostic producers — a plain rustc lint (unused_mut), rustdoc's invalid_html_tags, and a Clippy lint. The producing lint is irrelevant; the bug is in the shared snippet renderer, so the byte-offset values vary with the column layout of the offending line.
Reproduction
// repro.rs
fn main() {
// The line below triggers `unused_mut`; the trailing comment puts multibyte
// (non-ASCII) characters on the same line that the diagnostic underlines.
let mut x = 0; // 这是一段中文注释,用于在被下划线标注的行中加入多字节字符
let _ = x;
}
$ rustc --edition 2021 --error-format=json --json=diagnostic-rendered-ansi repro.rs
Expected: a normal unused_mut warning, emitted as JSON with a rendered field.
Actual: error: the compiler unexpectedly panicked with the backtrace above.
Notes:
- Whether a given line triggers the panic depends on the exact byte offset of the diagnostic span relative to the multibyte characters on that line (the renderer mixes byte indices and display columns). The example is representative; if it does not crash as-is on your machine, adding/removing a few characters before the span (so the underline start crosses a multibyte boundary) reliably surfaces it.
- In a real codebase, essentially any warning/error/lint emitted on a line that contains CJK (or other multibyte) text reproduces it under the JSON error format.
Workaround
$ rustc --error-format=short repro.rs # or: cargo <cmd> --message-format short
--error-format=short (and cargo … --message-format short) bypass the snippet renderer, so the real diagnostic is printed without the ICE.
Impact
- Any toolchain front-end that drives
rustc with the default --error-format=json --json=diagnostic-rendered-ansi (Cargo, Clippy, rustdoc, cargo-nextest, rust-analyzer, etc.) ICEs instead of reporting the diagnostic, as soon as one warning/error/lint lands on a source line containing multibyte characters.
- The crash happens in the renderer after the lint is produced, so
#[allow(...)] / --cap-lints do not prevent it (the diagnostic still has to be rendered).
- The first lint to hit such a line aborts the whole crate's diagnostic emission, masking every subsequent (and possibly the original) diagnostic — making the real problem very hard to locate without switching to
--message-format short.
This looks like an off-by-one / byte-vs-column indexing bug in annotate_snippets's StyledBuffer::replace (or its caller in renderer::render) when a rendered line contains characters whose UTF-8 byte length differs from their display width.
Summary
rustcpanics (ICE) inside the diagnostic renderer (annotate_snippets/StyledBuffer::replace) withslice index starts at N but ends at N-1whenever it has to render a diagnostic whose underlined source line contains multibyte UTF-8 characters (e.g. CJK), and the JSON error format is used (--error-format=json --json=diagnostic-rendered-ansi, which is what Cargo/Clippy/rustdoc/cargo-nextest/rust-analyzer pass by default).The panic is in the rendering of the
renderedfield of the JSON diagnostic, i.e. it happens after the warning/error/lint has already been produced. As a result, an ordinary, fixable diagnostic (e.g.unused_mut) turns into an opaquethe compiler unexpectedly panickedand the actual diagnostic is never shown.Version
--error-format=human/--error-format=shortare not affected (those paths do not go through the annotate-snippets snippet renderer).Panic message + backtrace (anonymized)
I observed the identical crash (same
StyledBuffer::replaceslice panic, with values such as13 but ends at 12,17 but ends at 16) from several different diagnostic producers — a plainrustclint (unused_mut),rustdoc'sinvalid_html_tags, and a Clippy lint. The producing lint is irrelevant; the bug is in the shared snippet renderer, so the byte-offset values vary with the column layout of the offending line.Reproduction
Expected: a normal
unused_mutwarning, emitted as JSON with arenderedfield.Actual:
error: the compiler unexpectedly panickedwith the backtrace above.Notes:
Workaround
--error-format=short(andcargo … --message-format short) bypass the snippet renderer, so the real diagnostic is printed without the ICE.Impact
rustcwith the default--error-format=json --json=diagnostic-rendered-ansi(Cargo, Clippy, rustdoc,cargo-nextest, rust-analyzer, etc.) ICEs instead of reporting the diagnostic, as soon as one warning/error/lint lands on a source line containing multibyte characters.#[allow(...)]/--cap-lintsdo not prevent it (the diagnostic still has to be rendered).--message-format short.This looks like an off-by-one / byte-vs-column indexing bug in
annotate_snippets'sStyledBuffer::replace(or its caller inrenderer::render) when a rendered line contains characters whose UTF-8 byte length differs from their display width.