[BUG] UTF8Util.isWellFormed inverts the surrogate-pair check: valid non-BMP characters are rejected, lone surrogates accepted
@efegokdemir がすでに取り組んでいます。
2026年9月24日 から。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 75/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- java
- 領域
- backend, networking
調査の方向性
バグは bifromq-util/src/main/java/org/apache/bifromq/util/UTF8Util.java の 45-57 行目にある UTF8Util.isWellFormed にあります。まず、UTF8UtilTest.java の 50 行目にあるメソッドと提供されたテストケースを読んでください。修正には、サロゲートペアのチェックを反転させ、単独のサロゲートを拒否し、有効な非 BMP 文字を受け入れることが含まれます。変更後、既存のテストを実行して合格することを確認し、絵文字のケースに対するテストアサーションを更新してください。
索引モデルが issue の本文から書いたものです。
説明
Environment
- BiFroMQ
4.0.0 - Verified against
main@ba2c34e0— still present on the latestmainat the time of filing - Module:
bifromq-util - File:
bifromq-util/src/main/java/org/apache/bifromq/util/UTF8Util.java:45-57
Summary
Character.isSurrogatePair(high, low) returns true when the two chars form a valid UTF-16 surrogate pair. The code treats that as a validation failure, inverting the check. As a result:
- well-formed non-BMP characters (emoji, CJK Extension B and above) are rejected, and
- unpaired/lone surrogates — the sequence the spec actually forbids — are accepted.
Code
for (int i = 1; i < len; i++) {
final char cr = str.charAt(i);
if (cr == '\u0000') {
return false;
}
if (Character.isSurrogatePair(cl, cr)) { // true only for a VALID pair
return false; // <-- rejects legal input
}
if (sanityCheck && isUnacceptableChar(cr)) {
return false;
}
cl = cr;
}
Evidence
UTF8Util.isWellFormed("😀", false); // U+1F600, a valid pair -> returns FALSE
UTF8Util.isWellFormed("a\uD83Db", false); // lone high surrogate -> returns TRUE
The first is a well-formed encoding of U+1F600; the second is malformed UTF-16 that the spec forbids.
Running these against the compiled bifromq-util classes on main @ ba2c34e0 gives exactly the above. A control with BMP text ("中文") correctly returns true, confirming the defect is specific to non-BMP handling.
Specification reference
- MQTT 5.0 [MQTT-1.5.4-1] and MQTT 3.1.1 [MQTT-1.5.3-1]: character data MUST be well-formed UTF-8 as defined by the Unicode specification / RFC 3629, and MUST NOT include encodings of code points between
U+D800andU+DFFF.
Non-BMP code points are encoded as 4-byte UTF-8 sequences and are therefore legal. Only the surrogate code points themselves are forbidden.
Impact
isWellFormed guards the following fields:
| Field | Call site |
|---|---|
| ClientId | MQTT3ConnectHandler.java:124, MQTT5ConnectHandler.java:155 |
| UserName | MQTT3ConnectHandler.java:135, MQTT5ConnectHandler.java:186 |
| Will topic | MQTT3ConnectHandler.java:139, MQTT5ConnectHandler.java:210 |
| Topic filter | MQTT5ProtocolHelper.java:250, :360 |
| Topic | MQTT5ProtocolHelper.java:539 |
| Response topic | MQTT5ProtocolHelper.java:589 |
Any client using an emoji or a CJK Extension B+ character in one of these fields is rejected as a protocol error and disconnected — an interoperability defect against both MQTT versions. (Common BMP CJK ideographs, U+4E00–U+9FFF, are unaffected; the impact is emoji and Extension B+ rare/ideographic characters, which occur in personal and place names.)
Note that the sibling validator isValidUTF8Payload, which goes through CharsetDecoder, handles lone surrogates correctly — the two validation paths disagree, which is further evidence that the logic above is inverted.
Why existing tests do not catch it
bifromq-util/src/test/java/org/apache/bifromq/util/UTF8UtilTest.java:50 asserts the incorrect behavior as expected:
assertFalse(UTF8Util.isWellFormed("hello😊world", false)); // surrogate pairs
😊 is a valid encoding of U+1F60A, so this assertion should read assertTrue. The test must be corrected together with the fix.
Suggested fix
Validate for unpaired surrogates instead, keeping the existing structure (every character is still checked for sanityCheck, as before):
char cl = str.charAt(0);
if (cl == '\u0000') {
return false;
}
if (Character.isLowSurrogate(cl)) {
return false; // unpaired low surrogate
}
if (sanityCheck && isUnacceptableChar(cl)) {
return false;
}
for (int i = 1; i < len; i++) {
final char cr = str.charAt(i);
if (cr == '\u0000') {
return false;
}
if (Character.isHighSurrogate(cl)) {
if (!Character.isLowSurrogate(cr)) {
return false; // unpaired high surrogate
}
} else if (Character.isLowSurrogate(cr)) {
return false; // unpaired low surrogate
}
if (sanityCheck && isUnacceptableChar(cr)) {
return false;
}
cl = cr;
}
return !Character.isHighSurrogate(cl); // trailing lone high surrogate
Note the trailing check after the loop: a high surrogate as the last character ("ab\uD83D") would otherwise pass, because cl is only validated when a following character exists. (Character.isDefined returns true for surrogate code points, so isUnacceptableChar never catches them — the pairing must be handled explicitly.)
- 主要言語
- Java
- スター
- 805
- フォーク
- 99
- 平均マージ
- 12時間 35分
- マージ済み PR(30日)
- 3
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
apache/bifromq のほかの issue
-
[BUG] KVRangeRestorer leaves the restore future uncompleted when startRestore() throws, permanently wedging the range対応中かも @ImDanXie が 35 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
-
[BUG] TopicUtil.isValidTopicFilter accepts "$share/g/" — a shared subscription with an empty filter that can never match対応中かも @efegokdemir が 36 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
Inbound and outbound packet-size checks differ, so a QoS 1 message just under MaxUserPayloadBytes is accepted but never delivered (blocks a persistent subscriber)再び着手できるかも @abhiramaab が 28 日前に担当しましたが、オープン中のプルリクエストはありません。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 55/100
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
-
[BUG] ManagedRequestPipeline.close() leaves in-flight request futures pending forever対応中かも @ImDanXie が 35 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
似ている issue
-
status: waiting-for-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
spring-projects/spring-framework#37614 ·
メンテナーはふだん 1 日以内に返信
-
azure-spring azure-spring-aad Client
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
Azure/azure-sdk-for-java#51046 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
helm-knative-apply.sh fails with Helm 4対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンarea: triage theme: undefined
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
jhipster/generator-jhipster#35895 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
oracle/graalvm-reachability-metadata#11659 ·
メンテナーはふだん 1 日以内に返信