Trojan Source: when code does not do what it looks like it does

· 6 min read

Unicode's bidirectional algorithm lets embedded control characters reorder how a line is displayed. Applied to source code, that means a reviewer can read a line as one thing while the compiler reads it as another. The controls are invisible, so the discrepancy is not apparent from the rendered text.

How the reordering works

Unicode has to display text that mixes right-to-left and left-to-right scripts, so it defines control characters that override the natural direction of a run: embeddings (U+202A, U+202B), overrides (U+202D, U+202E), a terminator (U+202C), and a newer set of isolates (U+2066–U+2069).

These characters are invisible and they affect display only. A compiler, interpreter or parser reads the underlying code point order. An editor shows the reordered result. When those two differ, a reviewer approves one thing and the machine runs another.

Detection

  • Directional overrides (U+202D, U+202E) have essentially no legitimate use in source code and should be treated as findings on sight.
  • Embeddings and isolates are legitimate in text that genuinely mixes scripts, so the signal there is imbalance — an opener without its matching terminator.
  • Counting has to be right: U+202C terminates embeddings and overrides alike, so a checker that counts overrides as non-openers while counting their terminators will report imbalances that do not exist.
  • Homoglyphs are the sibling problem: a Cyrillic "а" is a different identifier from a Latin "a" while looking identical. Flag mixed-script identifiers rather than silently substituting characters.

Practical defense

Scan for bidirectional controls in continuous integration and fail the build on directional overrides in source files. Most compilers now warn about them, but a check that runs before code review is better than one that runs after approval.

For text rather than code the same controls are worth reporting, though the judgment differs: a document legitimately mixing Arabic and English may use them correctly, so removal should be a considered choice rather than a default.

Keep reading