November 1, 2021·6 min read·engineeringrustsecurityunicodecompiler

Rust 1.56.1 rejects hidden text-direction controls in source

Rust's response to CVE-2021-42574 detects bidirectional controls that can mislead source reviewers. Visible escapes retain legitimate character values while exposing them in the source text.

Source review assumes that the code a reviewer sees has the structure the compiler will interpret. Invisible text-direction controls can undermine that assumption without changing the compiler's parsing rules. An editor can display characters in an order that obscures which text belongs to a literal or a comment. The resulting visual impression can differ from the program represented by the source.

The November 1 Rust security advisory identifies this issue as CVE-2021-42574. The Rust Security Response Working Group explicitly says it is not a flaw in rustc. Rust's response is a pair of deny-by-default lints in Rust 1.56.1, detecting the affected codepoints in literals and comments before compilation proceeds under the default lint settings.

I value Rust's ability to make program constraints visible, but the compiler cannot establish that a human has seen an accurate rendering of the source. These checks address that earlier review boundary. They make a class of hidden display instructions visible to the toolchain instead of leaving every reviewer to notice them unaided.

Text direction is a rendering feature

Unicode supports languages written left to right and right to left. Mixing those directions in one passage requires rules for presentation. The advisory describes invisible bidirectional override codepoints that help place words from one direction inside text using another direction.

Their legitimate purpose in ordinary human-language text does not prevent misuse in source. A control inside a literal or comment can influence how nearby text appears in an editor or review tool that implements bidirectional rendering. The compiler still receives the stored character sequence. The reviewer may see a presentation that suggests a different relationship between the characters.

The advisory's example uses visible placeholders for codepoints to explain a condition whose rendered appearance moves comment-like text. This avoids requiring readers to infer what an invisible character does from the display alone. Its point is the discrepancy between apparent and stored structure, not a new grammar rule that lets a comment execute.

The problem becomes especially serious if an entire team uses tools with the same misleading presentation. Independent review does not help much when each reviewer sees the same reordered text. The issue therefore belongs to the representation shared by review tools as well as to the repository's stored contents.

A conventional source diff can still contain the relevant change while making its significance hard to see. The attack does not require the compiler to accept an invalid program. It requires the review process to accept a valid program for the wrong visual reason. For a reviewer working from rendered text, checking syntax and inspecting appearance answer different questions. A compiler can accept the stored sequence according to its grammar while a human approves an interpretation suggested by the editor. The new lints add a check before that disagreement can pass through a default Rust build unnoticed, making the hidden control itself a diagnostic subject.

The compiler adds an observable failure

The advisory says Rust 1.0 through 1.56.0 lack the new lints. Rust 1.56.1 adds two that deny the affected characters by default, one for string literals and one for comments. A source file that previously compiled may now fail because it contains one of the controls.

That failure is intentional validation of source representation. It does not establish that the author intended an attack. A legitimate test or text-processing program may need the corresponding character value. The recommended response in that case is a visible escape sequence, and the compiler diagnostic suggests the appropriate spelling.

The Rust 1.56.1 literal-lint implementation makes the check concrete. Its list includes U+202A through U+202E and U+2066 through U+2069. It searches the text for these values and identifies their spans for a diagnostic. Its declared default is Deny.

The implementation also handles character literals and documentation comments through its early lint pass. Byte strings already have a different restriction on non-ASCII characters, as the code's comment explains. These details show why a compiler-level check can provide more context than a visual search for suspicious glyphs.

Escapes preserve the value without hiding its spelling

When a program needs a direction-control character, an escaped literal can represent it without placing the invisible control directly in the source. This example uses a Rust Unicode escape for a right-to-left override. The source spelling consists of visible characters.

let right_to_left_override = '\u{202e}';

The literal still denotes the Unicode scalar value. Its source representation no longer asks the editor to apply that value as a raw text-direction instruction. A reviewer can identify the escape and inspect why the program needs it.

That distinction separates source text from runtime text. A test of Unicode handling may intentionally construct a string containing direction controls. Requiring visible source escapes does not forbid the resulting data. It changes the way the author declares the data so its presence is reviewable.

The implementation offers different suggestions depending on context. Where it can identify inner spans, it can suggest removing the controls or replacing them with escaped spellings. For some documentation-comment handling it emits explanatory notes instead. The diagnostic behavior follows what the compiler knows about the original text location.

I prefer this approach for legitimate test cases because it records intent in the source. A named variable containing an explicit escape is easier to assess than an apparently ordinary string with invisible presentation behavior. The program can exercise the same character while keeping that choice inspectable.

The repository boundary extends beyond Rust files

The advisory recommends periodic checking of the listed codepoints in the repository and its dependencies when an upgrade is unavailable. It also recommends that approach for codebases containing non-Rust source. A Rust lint does not establish the contents of every other file a project reviews or executes.

Dependencies matter because source review is not limited to code written locally. A misleading display in external source can influence an audit or a proposed dependency update. Checking only the application's own modules would leave that part of the source population outside the stated mitigation.

The list of codepoints gives such an inspection a defined target. A blanket rejection of every non-ASCII character would address a much larger population, including ordinary identifiers and human-language text. The advisory instead identifies specific presentation controls relevant to this issue.

The distinction is important for a language that has recently added Unicode identifiers. Supporting more scripts in identifiers and detecting invisible direction controls are compatible choices. The diagnostic concerns particular effects on presentation, not a claim that every name outside ASCII is unsafe.

The ecosystem scan has a dated scope

The response group analyzed every crate version published to crates.io as of October 17. It reports five crates containing the affected codepoints and says none of those occurrences were malicious. This is a useful observation with an explicit cutoff date and source population.

It is not evidence about every private repository or every source file published after the scan. It also does not eliminate the mechanism described by the researchers. A defensive measure can be justified even when a particular ecosystem inspection finds no malicious use.

The advisory credits Nicholas Boucher and Ross Anderson with reporting the issue. Its timeline begins with the July 25 report and ends with the November 1 coordinated disclosure and patch release. That history explains why a new compiler check can arrive together with public explanation of a rendering problem affecting several kinds of tooling.

Similar-looking identifiers are a separate issue

The advisory's appendix distinguishes homoglyph attacks, assigned CVE-2021-42694. Those concern identifiers that look alike, rather than the hidden direction controls at issue here. Rust already has mitigations for that identifier problem from Rust 1.53, when non-ASCII identifiers became available.

Earlier releases through 1.52.1 do not support those identifiers and are not affected by that particular identifier attack, according to the appendix. They still lack the new text-direction lints. The two issues therefore have different version histories even though both exploit a gap between appearance and encoded text.

Rust 1.56.1 makes the listed hidden controls a compiler-visible problem by default. Legitimate values can use explicit escapes, and repositories containing other languages can apply the advisory's codepoint inspection beyond Rust compilation. Those mechanisms address the representation a reviewer examines while leaving the compiler's ordinary interpretation of source intact.