[BUG] UTF8Util.isWellFormed inverts the surrogate-pair check: valid non-BMP characters are rejected, lone surrogates accepted
@efegokdemir ci sta già lavorando.
Dal 24/9/2026.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 75/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- java
- Ambito
- backend, networking
Direzione di ricerca
Il bug si trova in UTF8Util.isWellFormed in bifromq-util/src/main/java/org/apache/bifromq/util/UTF8Util.java righe 45-57. Inizia leggendo il metodo e il caso di test fornito in UTF8UtilTest.java riga 50. La correzione comporta l'inversione del controllo delle coppie di surrogati per rifiutare i surrogati solitari e accettare i caratteri non-BMP validi. Dopo aver apportato la modifica, esegui i test esistenti per assicurarti che passino e aggiorna l'asserzione del test per il caso dell'emoji.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.)
- Lingua principale
- Java
- Stelle
- 805
- Fork
- 99
- Merge medio
- 12h 35m
- PR unite (30g)
- 3
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di apache/bifromq
-
[BUG] KVRangeRestorer leaves the restore future uncompleted when startRestore() throws, permanently wedging the rangeForse già presa @ImDanXie l’ha presa 34 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
[BUG] TopicUtil.isValidTopicFilter accepts "$share/g/" — a shared subscription with an empty filter that can never matchForse già presa @efegokdemir l’ha presa 35 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 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)Forse di nuovo libera @abhiramaab l’ha presa 27 giorni fa e non c’è nessuna pull request aperta. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
-
[BUG] ManagedRequestPipeline.close() leaves in-flight request futures pending foreverForse già presa @ImDanXie l’ha presa 34 giorni fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 68/100
Tutte le issue di apache/bifromq
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
oracle/graalvm-reachability-metadata#11472 ·
I maintainer di solito rispondono entro 1 giorno
-
OkHttp versions 5.4.0+ no longer include baseline-prof.txtForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertabug
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 83/100
lysine-dev/okhttp#9828 ·
I maintainer di solito rispondono entro 1 giorno
-
status: waiting-for-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
spring-projects/spring-ai#7269 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 89/100
objectionary/hone-maven-plugin#1353 ·
I maintainer di solito rispondono entro 9 giorni
-
ClassNotFoundException: io.netty.channel.kqueue.KQueueEventLoop stack trace printed during native build on main (Netty 4.2 removed the class)Forse già presa @gastaldi l’ha presa oggi. Apertaarea/netty kind/bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
quarkusio/quarkus#57939 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno