#37647·React

[Compiler Bug]: Rust backend decodes user text that looks like `__SURROGATE_XXXX__` into a lone surrogate

Author: kakiuwang-uiCreated Sep 17, 2026Updated Sep 17, 2026

What kind of issue is this?

  • React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)

Link to repro

No playground link: this only affects the Rust backend (babel-plugin-react-compiler-rust). The fixture below reproduces it with yarn snap --rust on main (2b19aec).

Repro steps

The Rust bridge encodes lone surrogates in the AST JSON as __SURROGATE_XXXX__ text (sanitizeJsonSurrogates in bridge.ts). The decoders, JsString::from_marker_string on the Rust side and restoreJsonSurrogates on the way back, turn any __SURROGATE_XXXX__ text into a surrogate, including text the user actually wrote. Existing text that looks like a marker is never escaped, so it gets decoded too.

import {Stringify} from 'shared-runtime';

function foo() {
  return <Stringify value={['__SURROGATE_D83D__']} />;
}

export const FIXTURE_ENTRYPOINT = {
  fn: foo,
  params: [],
  isComponent: false,
};

yarn snap --rust:

Non-forget (expected):
(kind: ok) <div>{"value":["__SURROGATE_D83D__"]}</div>
Forget:
(kind: ok) <div>{"value":["\ud83d"]}</div>

The string silently becomes a lone surrogate, with no error. The same happens when the text is built from pieces that are folded, e.g. '__SURROGATE_' + 'D83D__' or a template literal. The TS backend is not affected. Only uppercase hex matches the marker pattern.

Any in-band text marker needs an escape for itself, so fixing this means changing the wire format on both sides. Two options I can see:

  1. Keep the markers and escape existing marker-like text before encoding (e.g. rewrite a literal __SURROGATE_ prefix into a separate escape marker). Then undo that escape in from_marker_string, to_marker_string and restoreJsonSurrogates. This is a small change, but all four functions have to agree.
  2. Stop sending lone surrogates as text: pass them out of band (for example as a list of positions and code units next to the JSON), or use an encoding that serde can read directly. This removes the ambiguity, but it's a bigger change to the bridge.

Happy to send a PR once there's a preferred approach.

How often does this bug happen?

Every time

What version of React are you using?

main (2b19aec)

What version of React Compiler are you using?

main (2b19aec), Rust backend