Skip to content

Fix modified-UTF-8 corruption in JNI string marshaling - #1

Merged
Thrameos merged 3 commits into
masterfrom
fix/modified-utf8-jni-strings
Sep 6, 2026
Merged

Thrameos merged 3 commits into
masterfrom
fix/modified-utf8-jni-strings

Conversation

@Thrameos

@Thrameos Thrameos commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #6

Summary

  • GetJNIString()/NewJNIString() in native/jni_util.cpp used env->GetStringUTFChars()/env->NewStringUTF(), JNI's modified UTF-8 (CESU-8) encoding, but treated the result as standard UTF-8 on the CefString side. Supplementary-plane characters (emoji, etc.) and embedded NULs were silently corrupted crossing the JNI boundary in either direction.
  • Fix goes through JNI's non-UTF string functions (NewString()/GetStringChars()) instead, copying CefString's native UTF-16 buffer directly (jchar/char16_t are both exactly 16 bits) -- no transcoding, no ambiguity.
  • Source-compatible with all existing call sites (NewJNIString/ScopedJNIString now take const CefString& instead of const std::string&; every call site already passed a CefString or a literal that constructs one implicitly).
  • Added ModifiedUtf8Test.java, reproducing both directions of the bug in isolation (native->Java via onTitleChange, Java->native via executeJavaScript, each checked so a bug in one direction can't be masked by the other) -- both failed before the fix, both pass after.
  • Added TestFrame.createBrowser(url, useOSR) overload and OsrSmokeTest.java: the new regression tests use an OSR (off-screen-rendered) browser rather than TestFrame's default windowed mode, since OSR closes via a pure Java-side handshake. This sidesteps an unrelated, separately-reproduced windowed-browser close hang in this dev environment that matches upstream java-cef#364 (not fixed by or related to this PR).

Test plan

  • Rebuilt libjcef.so cleanly (ninja jcef), zero compile errors.
  • ModifiedUtf8Test reproduced the bug pre-fix (mangled title ð, FAIL:fffd replacement-character corruption) and passes post-fix.
  • Re-ran alongside OsrSmokeTest (existing OSR browser lifecycle coverage) -- no regression.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BLqFiEqAPbHc3MifgpNjA8

Thrameos and others added 2 commits September 5, 2026 20:03
GetJNIString()/NewJNIString() used env->GetStringUTFChars()/NewStringUTF(),
which use JNI's "modified UTF-8" (CESU-8) encoding: supplementary-plane
characters (e.g. emoji) are encoded as a pair of 3-byte surrogate
sequences instead of one 4-byte UTF-8 sequence, and NUL is encoded as an
overlong 2-byte sequence. The result was treated as standard UTF-8 by
CefString, silently corrupting any string containing a supplementary-plane
character or an embedded NUL crossing the JNI boundary in either
direction.

Since CefString is UTF-16 internally with direct buffer access, and
jchar/char16_t are both exactly 16 bits, the fix goes through JNI's
non-UTF string functions (NewString()/GetStringChars()) instead, copying
UTF-16 directly with no encoding ambiguity and no transcoding needed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BLqFiEqAPbHc3MifgpNjA8
ModifiedUtf8Test covers both directions of the bug fixed in the previous
commit, isolated so a bug in one direction can't be masked by the other:
- nativeToJavaAstralCharacterSurvivesTitleChange: a supplementary-plane
  character generated entirely inside JS (String.fromCodePoint) is read
  back into Java via onTitleChange, exercising NewJNIString().
- javaToNativeAstralCharacterSurvivesExecuteJavaScript: a Java string
  containing the same character is passed to executeJavaScript() and
  verified inside JS (result reported back as a plain-ASCII PASS/FAIL),
  exercising GetJNIString() without the readback itself going back
  through NewJNIString().

Both tests use a new OSR (off-screen-rendered) browser via TestFrame's
new createBrowser(url, useOSR) overload, added because OSR browsers close
via a pure Java-side handshake and don't depend on native window-destroy
notification -- unlike TestFrame's default windowed mode, whose close
path currently hangs in this environment (tracked separately, matches
upstream java-cef#364; unrelated to this fix).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BLqFiEqAPbHc3MifgpNjA8
@Thrameos
Thrameos force-pushed the fix/modified-utf8-jni-strings branch from 992e168 to 609f172 Compare September 6, 2026 03:04
@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.87755% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.62%. Comparing base (a4d0d58) to head (6b0dfe6).
⚠️ Report is 13 commits behind head on master.

Files with missing lines Patch % Lines
native/jni_util.cpp 85.71% 1 Missing and 1 partial ⚠️
java/tests/junittests/ModifiedUtf8Test.java 97.14% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master       #1      +/-   ##
============================================
+ Coverage     39.61%   41.62%   +2.01%     
- Complexity      761      868     +107     
============================================
  Files           248      253       +5     
  Lines         14990    15773     +783     
  Branches       2461     2650     +189     
============================================
+ Hits           5938     6566     +628     
- Misses         7813     7895      +82     
- Partials       1239     1312      +73     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

javaToNativeAstralCharacterSurvivesExecuteJavaScript() terminated on the
first onTitleChange call, which can be an initial title defaulting to the
page URL before executeJavaScript()'s title assignment actually lands --
same class of flake DisplayHandlerTest already had to guard against
(onTitleChange/onAddressChange firing more than once). Filter to only the
JS-assigned "PASS"/"FAIL:..." value.

Found via a real CI failure on this PR after rebasing onto master.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HBFmQs6JDypYP5qdC99yDq
@Thrameos
Thrameos merged commit 5012785 into master Sep 6, 2026
16 of 17 checks passed
@Thrameos
Thrameos deleted the fix/modified-utf8-jni-strings branch September 6, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modified UTF-8 (CESU-8) corruption in JNI string marshaling (GetJNIString/NewJNIString)

1 participant