RaTeX is a KaTeX-compatible math rendering engine written in Rust. Prior to version 0.1.11, the public parser entrypoint…
GitHub_M·CWE-248·Published 2026-07-07
RaTeX is a KaTeX-compatible math rendering engine written in Rust. Prior to version 0.1.11, the public parser entrypoint `ratex_parser::parse(&str)` panics on the 9-byte input `\verbéxé` (i.e. `\verb` followed by the non-ASCII delimiter `é`). When handling a `\verb` command, the parser slices the verbatim argument with byte indices (`arg[1..arg.len() - 1]`); if the delimiter character is multibyte UTF-8, index `1` lands inside that character and Rust panics with *“byte index 1 is not a char boundary”*. Because RaTeX’s release profile sets `panic = "abort"` (`Cargo.toml:48`), the panic aborts the entire process — not just the current request/thread — making this a hard denial of service for any service that renders untrusted LaTeX. Version 0.1.11 fixes the issue.
RaTeX is a KaTeX-compatible math rendering engine written in Rust. Prior to version 0.1.11, the public parser entrypoint `ratex_parser::parse(&str)` panics on the 9-byte input `\verbéxé` (i.e. `\verb` followed by the non-ASCII delimiter `é`). When handling a `\verb` command, the parser slices the verbatim argument with byte indices (`arg[1..arg.len() - 1]`); if the delimiter character is multibyte UTF-8, index `1` lands inside that character and Rust panics with *“byte index 1 is not a char boundary”*. Because RaTeX’s release profile sets `panic = "abort"` (`Cargo.toml:48`), the panic aborts the entire process — not just the current request/thread — making this a hard denial of service for any service that renders untrusted LaTeX. Version 0.1.11 fixes the issue.
### Summary The public parser entrypoint `ratex_parser::parse(&str)` panics on the **9-byte** input `\verbéxé` (i.e. `\verb` followed by the non-ASCII delimiter `é`). When handling a `\verb` command, the parser slices the verbatim argument with **byte** indices (`arg[1..arg.len() - 1]`); if the delimiter character is multibyte UTF-8, index `1` lands inside that character and Rust panics with *“byte index 1 is not a char boundary”*. Because RaTeX’s release profile sets `panic = "abort"` (`Cargo.toml:48`), the panic aborts the **entire process** — not just the current request/thread — making this a hard denial of service for any service that renders untrusted LaTeX. ### Details ## Affected code `crates/ratex-parser/src/parser.rs`, `parse_symbol_inner`: ```rust if let Some(stripped) = text.strip_prefix("\\verb") { // parser.rs:901 self.consume(); let arg = stripped.to_string(); // e.g. "éxé" let star = arg.starts_with('*'); let arg = if star { &arg[1..] } else { &arg }; // parser.rs:905 (also byte-sliced) if arg.len() < 2 { // byte length return Err(ParseError::new("\\verb assertion failed", Some(&nucleus))); } let body = arg[1..arg.len() - 1].to_string(); // parser.rs:910 <-- PANIC on multibyte delimiter ... } ``` For input `\verbéxé`: `arg = "éxé"`, where `é` = `U+00E9` (bytes `C3 A9`). `arg.len()` is the **byte** length (5), the `< 2` guard passes, and `arg[1..4]` starts at byte index 1 — inside the first `é` (bytes 0..2) — so the slice panics. The lexer groups `\verb<delim>…<delim>` correctly with char semantics (`lexer.rs` `lex_verb`); only the parser mishandles it. ### PoC <img width="1109" height="205" alt="image" src="https://github.com/user-attachments/assets/cd4bc6ae-23dd-458f-826c-6ce4e85c7005" /> ``` $ printf '\\verb\xc3\xa9x\xc3\xa9\n' | ./target/release/parse thread 'main' panicked at crates/ratex-parser/src/parser.rs:910:27: start byte index 1 is not a char boundary; it is inside 'é' (bytes 0..2 of string) Aborted (core dumped) # exit 134 — panic=abort kills the whole process ``` ### Impact Any application that renders untrusted LaTeX through RaTeX (web “render this math” endpoint, WASM in-browser use, the FFI embedded in another app) can be crashed by a tiny string. With `panic = "abort"` in release builds, the crash takes down the whole process / server, so a single malicious formula causes a full-service DoS (and, in batch pipelines, drops all queued work). ## Remediation Slice by character boundaries instead of byte indices, mirroring the UTF-8-correct logic the lexer already uses. For example: ```rust let chars: Vec<char> = arg.chars().collect(); if chars.len() < 2 { return Err(ParseError::new("\\verb assertion failed", Some(&nucleus))); } let body: String = chars[1..chars.len() - 1].iter().collect(); ``` (Apply the same char-aware handling to the `*` strip at `parser.rs:905`.) More broadly, consider not using `panic = "abort"` for builds embedded in long-running services, and/or wrapping parsing in `catch_unwind` at the FFI/WASM boundary — but the byte-slice fix is the direct correction.
### Summary The public parser entrypoint `ratex_parser::parse(&str)` panics on the **9-byte** input `\verbéxé` (i.e. `\verb` followed by the non-ASCII delimiter `é`). When handling a `\verb` command, the parser slices the verbatim argument with **byte** indices (`arg[1..arg.len() - 1]`); if the delimiter character is multibyte UTF-8, index `1` lands inside that character and Rust panics with *“byte index 1 is not a char boundary”*. Because RaTeX’s release profile sets `panic = "abort"` (`Cargo.toml:48`), the panic aborts the **entire process** — not just the current request/thread — making this a hard denial of service for any service that renders untrusted LaTeX. ### Details ## Affected code `crates/ratex-parser/src/parser.rs`, `parse_symbol_inner`: ```rust if let Some(stripped) = text.strip_prefix("\\verb") { // parser.rs:901 self.consume(); let arg = stripped.to_string(); // e.g. "éxé" let star = arg.starts_with('*'); let arg = if star { &arg[1..] } else { &arg }; // parser.rs:905 (also byte-sliced) if arg.len() < 2 { // byte length return Err(ParseError::new("\\verb assertion failed", Some(&nucleus))); } let body = arg[1..arg.len() - 1].to_string(); // parser.rs:910 <-- PANIC on multibyte delimiter ... } ``` For input `\verbéxé`: `arg = "éxé"`, where `é` = `U+00E9` (bytes `C3 A9`). `arg.len()` is the **byte** length (5), the `< 2` guard passes, and `arg[1..4]` starts at byte index 1 — inside the first `é` (bytes 0..2) — so the slice panics. The lexer groups `\verb<delim>…<delim>` correctly with char semantics (`lexer.rs` `lex_verb`); only the parser mishandles it. ### PoC <img width="1109" height="205" alt="image" src="https://github.com/user-attachments/assets/cd4bc6ae-23dd-458f-826c-6ce4e85c7005" /> ``` $ printf '\\verb\xc3\xa9x\xc3\xa9\n' | ./target/release/parse thread 'main' panicked at crates/ratex-parser/src/parser.rs:910:27: start byte index 1 is not a char boundary; it is inside 'é' (bytes 0..2 of string) Aborted (core dumped) # exit 134 — panic=abort kills the whole process ``` ### Impact Any application that renders untrusted LaTeX through RaTeX (web “render this math” endpoint, WASM in-browser use, the FFI embedded in another app) can be crashed by a tiny string. With `panic = "abort"` in release builds, the crash takes down the whole process / server, so a single malicious formula causes a full-service DoS (and, in batch pipelines, drops all queued work). ## Remediation Slice by character boundaries instead of byte indices, mirroring the UTF-8-correct logic the lexer already uses. For example: ```rust let chars: Vec<char> = arg.chars().collect(); if chars.len() < 2 { return Err(ParseError::new("\\verb assertion failed", Some(&nucleus))); } let body: String = chars[1..chars.len() - 1].iter().collect(); ``` (Apply the same char-aware handling to the `*` strip at `parser.rs:905`.) More broadly, consider not using `panic = "abort"` for builds embedded in long-running services, and/or wrapping parsing in `catch_unwind` at the FFI/WASM boundary — but the byte-slice fix is the direct correction.
RaTeX es un motor de renderizado de matemáticas compatible con KaTeX escrito en Rust. Antes de la versión 0.1.11, el punto de entrada del analizador público 'ratex_parser::parse(&str)' entra en pánico con la entrada de 9 bytes '\verbéxé' (es decir, '\verb' seguido por el delimitador no ASCII 'é'). Al manejar un comando '\verb', el analizador segmenta el argumento verbatim con índices de bytes ('arg[1..arg.len() - 1]'); si el carácter delimitador es UTF-8 multibyte, el índice '1' cae dentro de ese carácter y Rust entra en pánico con 'byte index 1 is not a char boundary'. Debido a que el perfil de lanzamiento de RaTeX establece 'panic = “abort”' ('Cargo.toml:48'), el pánico aborta todo el proceso - no solo la solicitud/hilo actual - haciendo de esto una dura denegación de servicio para cualquier servicio que renderice LaTeX no confiable. La versión 0.1.11 corrige el problema.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 4.0 | Primary | cve.org | 8.7 | — | — | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Primary | cve.org | 8.7 | — | — |
| CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | GHSA | 8.7 | — | — | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | NVD | 8.7 | — | — | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X |
| 4.0 | Secondary | ENISA EUVD | 8.7 | — | — | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |