Skip to content

Fix Text truncation at NULL character on iOS and Android (#24129) - #57906

Closed
kulkarni-rohan wants to merge 1 commit into
react:mainfrom
kulkarni-rohan:fix/24129-null-char
Closed

kulkarni-rohan wants to merge 1 commit into
react:mainfrom
kulkarni-rohan:fix/24129-null-char

Conversation

@kulkarni-rohan

Copy link
Copy Markdown
Contributor

Fixes #24129

Summary:

Problem: <Text>{'Hello \u0000 World'}</Text> renders as "Hello" (truncated at \u0000). Does NOT reproduce when Debug JS Remotely (JSON preserves \u0000), but reproduces in production Hermes+Fabric on iOS and Android.

Root cause: JS String preserves \u0000 (length includes it), C++ std::string from JSI utf8() also preserves via size. Truncation happens at platform bridge where C-string NUL-terminated APIs are used:

  • iOS: RCTAttributedTextUtils.mm:420 stringWithUTF8String:fragment.string.c_str() stops at embedded \0
  • iOS: RCTConversions.h RCTNSStringFromString via stringWithCString: and reverse std::string{UTF8String}
  • iOS: RCTTurboModule.mm convertJSIStringToNSString same
  • Android: JavaTurboModule.cpp NewStringUTF(c_str()) expects NUL-terminated

Fix (4 files, ~10 lines, no new API): Replace C-string APIs with length-aware [[NSString alloc] initWithBytes:data length:size encoding:NSUTF8StringEncoding] (same as existing correct pattern in FollyConvert.mm:31 and MapBufferBuilder.cpp) and Android convertUTF8ToUTF16 + NewString(jchar*, len) + assign(jsChars, GetStringUTFLength()). All surfaces (Text, TextInput, accessibility, TurboModule params) fixed at once because shared converters are fixed.

Changelog:

[GENERAL] [FIXED] - Fix Text truncation when string contains NULL character \u0000 (#24129)

Test Plan:

Reproduction (RNTester):

  • Add screen Text > NullCharacter with {'Hello\u0000World'} and 'A\u0000B\u0000C'
  • Before: "Hello" truncated
  • After: "HelloWorld" full (invisible \0 zero-width but World visible, length preserved)

Commands:

yarn lint
# → Done (max-warnings 0)

# Text itest requires buck, but JS unit for length preservation:
node -p "'a\u0000b'.length" # → 3

Closes #24129

…t#24129)

Fixes react#24129 where <Text>{'Hello \u0000 World'}</Text> renders as 'Hello' truncated.

Root cause: JS String preserves \u0000, but iOS stringWithUTF8String: and stringWithCString: and Android NewStringUTF use C-string NUL-terminated APIs that stop at embedded \0.

Fix: Replace with length-aware conversions:
- iOS: initWithBytes:length:encoding: (same as correct FollyConvert.mm pattern and MapBufferBuilder memcpy)
- Android: convert UTF-8 std::string with size to UTF-16 via rt.utf16() + NewString, and reverse via GetStringChars + GetStringLength + createFromUtf16

Covers all surfaces (Text, TextInput, any TurboModule string param) across Fabric and old arch.

[GENERAL] [FIXED] - Fix Text truncation when string contains NULL character \u0000 (react#24129)
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 12, 2026
@kulkarni-rohan kulkarni-rohan changed the title Fix Text truncation at NULL character \u0000 on iOS and Android (#24129) Fix Text truncation at NULL character on iOS and Android (#24129) Aug 12, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Aug 12, 2026
@meta-codesync

meta-codesync Bot commented Aug 12, 2026

Copy link
Copy Markdown

@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D115707536.

@meta-codesync meta-codesync Bot closed this in 5906cfb Aug 12, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Aug 12, 2026
@meta-codesync

meta-codesync Bot commented Aug 12, 2026

Copy link
Copy Markdown

@fabriziocucci merged this pull request in 5906cfb.

MelvinBot added a commit to Expensify/App that referenced this pull request Sep 17, 2026
…reasons

The NUL-truncation bug was already fixed upstream in react/react-native#57906
(merged as 5906cfb, ships in v0.88.0). Replace the hand-rolled single-file patch
with a verbatim backport of that commit so it can be dropped on the 0.88 upgrade
without re-deriving anything, and shorten the details.md Reason entries for
patches 042 and 043 to the essentials.

Co-authored-by: daledah <daledah@users.noreply.github.com>
tvinhas pushed a commit to tvinhas/react-native-macos that referenced this pull request Oct 21, 2026
react#57906)

Summary:
Fixes react#24129

**Problem:** `<Text>{'Hello \u0000 World'}</Text>` renders as "Hello" (truncated at \u0000). Does NOT reproduce when Debug JS Remotely (JSON preserves \u0000), but reproduces in production Hermes+Fabric on iOS and Android.

**Root cause:** JS String preserves \u0000 (length includes it), C++ std::string from JSI utf8() also preserves via size. Truncation happens at platform bridge where C-string NUL-terminated APIs are used:
- iOS: RCTAttributedTextUtils.mm:420 stringWithUTF8String:fragment.string.c_str() stops at embedded \0
- iOS: RCTConversions.h RCTNSStringFromString via stringWithCString: and reverse std::string{UTF8String}
- iOS: RCTTurboModule.mm convertJSIStringToNSString same
- Android: JavaTurboModule.cpp NewStringUTF(c_str()) expects NUL-terminated

**Fix (no new API):** Replace the C-string APIs with length-aware equivalents.

On iOS, `[[NSString alloc] initWithBytes:length:encoding:]` for std::string to NSString, and `dataUsingEncoding:` for the reverse, matching the existing correct pattern in FollyConvert.mm:31 and MapBufferBuilder.cpp.

On Android the conversion now goes through UTF-16 in both directions rather than UTF-8:
- outbound: `rt.utf16(...)` then `NewString(jchar*, len)`
- inbound: `GetStringLength()` + `GetStringChars()` then `jsi::String::createFromUtf16(...)`, released with `ReleaseStringChars`

That is worth calling out because it fixes a second latent bug. `NewStringUTF` expects *modified* UTF-8 (CESU-8), so it already mishandled 4-byte sequences such as emoji and other supplementary-plane characters. Going UTF-16 to UTF-16 avoids both problems.

All surfaces (Text, TextInput, accessibility, TurboModule params) are fixed at once because the shared converters are fixed.

## NOTE: nil becomes empty string at two call sites

RCTTurboModule.mm and RCTAttributedTextUtils.mm gain a `?: @""` fallback. Previously `stringWithUTF8String:` returned `nil` for invalid UTF-8 and callers received nil; they now receive `@""`. This matches the fallback `RCTNSStringFromString` already had, and `@""` is safer than nil for the ObjC call sites involved, but it is a behaviour change rather than a pure refactor.

## Changelog:
[GENERAL] [FIXED] - Fix Text truncation when string contains NULL character \u0000 (react#24129)

Pull Request resolved: react#57906

Test Plan:
**Reproduction (RNTester):**
- Add screen Text > NullCharacter with <Text>{'Hello\u0000World'}</Text> and 'A\u0000B\u0000C'
- Before: "Hello" truncated
- After: "HelloWorld" full (invisible \0 zero-width but World visible, length preserved)

Closes react#24129

## Added on import: regression tests

Four cases added to the existing `React/Tests/Text/RCTAttributedTextUtilsTest.mm`, which is owned by `TextTestsApple`:

```
buck2 test fbsource//xplat/js/react-native-github:TextTestsApple
→ Pass 31. Fail 0.
```

Restoring the pre-fix `RCTConversions.h` and `RCTAttributedTextUtils.mm` and re-running fails all four and nothing else:

```
✗ RCTAttributedTextUtilsTest/testNSStringFromStringPreservesEmbeddedNull
✗ RCTAttributedTextUtilsTest/testStringFromNSStringPreservesEmbeddedNull
✗ RCTAttributedTextUtilsTest/testStringConversionRoundTripsEmbeddedNull
✗ RCTAttributedTextUtilsTest/testAttributedStringFromFragmentPreservesEmbeddedNull
Tests finished: Pass 27. Fail 4.
```

They cover `RCTNSStringFromString`, `RCTStringFromNSString`, a round trip through both, and the real AttributedString to NSAttributedString path. The changed function in RCTAttributedTextUtils.mm (`RCTNSAttributedStringFragmentFromFragment`) is static, so that last one goes through the public `RCTNSAttributedStringFromAttributedString`.

This replaces the original `node -p "'a\u0000b'.length"` check, which exercised JavaScript string length rather than any of the changed code.

The Android hunks are not covered by these tests. They are iOS-only test targets and there is no equivalent JNI-level unit test in tree, so the Android side rests on code review.

## On the reverse-direction allocation

`RCTStringFromNSString` now allocates an `NSData` where it previously used `UTF8String` (an interior pointer). That converter has 6 callers. The hot one is `RCTNSStringFromString` with 43 callers, and it does not add an allocation: `stringWithCString:` and `initWithBytes:` both allocate an NSString. Keeping the `NSData` form deliberately, because the cheaper alternative relies on `UTF8String`'s buffer containing embedded NULs, which is exactly the ambiguity this diff removes.

`arc lint -e extra` on the test file reports only pre-existing warnings plus a NULLSAFECLANG infrastructure failure.

Reviewed By: cipolleschi

Differential Revision: D115707536

Pulled By: fabriziocucci

fbshipit-source-id: 4cde8d4b584e0305fcfc3a4520f3690b411500cb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. p: Facebook Partner: Facebook Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JavaScript strings with NULL character are not handled properly

1 participant