[BUG] bionic: wcscspn() en wcspbrk() behandelen elke wchar_t >= 0x100 als een match (Android 17, CP3A.260905.009)

Beschrijving

Elke code die wide strings scant op ASCII-scheidingstekens (zoals nieuwe regels of wildcard-tekens) raakt defect zodra de tekst een emoji, CJK-tekens, Cyrillisch, Grieks of tekens uit de Latin Extended-set bevat.

Beïnvloede en niet-beïnvloede apparaten

ApparaatBuildResultaat
Pixel 7google/panther/panther:17/CP3A.260905.009/16091614:user/release-keysDefect
Pixel 8agoogle/akita/akita:17/CP3A.260905.009/16091614:user/release-keys (patch 2026-09-05)Defect
Nothing Phone 2Nothing/PongEEA/Pong:16/BQ2A.250721.001-BP2A.250605.031.A3 (patch 2026-08-01)OK

De publieke android17-release bronnen (android-17.0.0r1, juni 2026) bouwen wcscspn/wcspbrk vanuit external/llvm-libc, wat een eenvoudige geneste loop over wchart is en correct werkt. De code in de september-builds is anders, waardoor de regressie tussen de juni- en september-releases is geïntroduceerd.

Reproductie

De volgende code kan worden gecompileerd met de NDK (aarch64-linux-android30-clang), naar /data/local/tmp worden gepusht en worden uitgevoerd via adb shell:

#include <stdio.h>
#include <wchar.h>

int main(void)
{
    wchar_t s[] = {'a', 0x1F680, 0x100, 'b', 'x', '\n', 0};
    printf("wcscspn(s, L\"x\\n\") = %zu (expected 4)\n", wcscspn(s, L"x\n"));
    printf("wcspbrk(s, L\"x\\n\") = index %ld (expected 4)\n", (long)(wcspbrk(s, L"x\n") - s));
    printf("wcsspn(s, L\"a\\U0001F680\") = %zu (expected 2)\n", wcsspn(s, L"a\U0001F680"));
    return 0;
}

Output op CP3A.260905.009:

  • wcscspn(s, L"x\n") = 1 (expected 4)
  • wcspbrk(s, L"x\n") = index 1 (expected 4)
  • wcsspn(s, L"a\U0001F680") = 2 (expected 2)

Een scan van elk codepunt van U+0001 tot U+10FFFF (met uitzondering van surrogaten en de twee set-leden) met wcscspn(L"<cp>\n", L"x\n") geeft voor 1.111.808 codepunten een 0 terug in plaats van 1: elk punt van U+0100 tot U+10FFFF. De waarden U+0001 tot U+00FF zijn correct.

Triggercondities (gemeten op Pixel 8a)

Afwijzingsset (Reject set)Resultaat
Eén tekenOK
Twee of meer tekens, allemaal $<$ U+0100 (bijv. L"x\n", L"\xe9\n")Defect
Twee of meer tekens, minstens één $\ge$ U+0100OK
Haystack beperkt tot U+0080..U+00FFOK

Mechanisme (gebaseerd op de geleverde libc.so)

Disassemblage van wcscspn in /apex/com.android.runtime/lib64/bionic/libc.so op CP3A.260905.009 laat drie paden zien die worden geselecteerd door de set:

  1. Eén teken: Een eenvoudige vergelijking-loop. Correct.
  2. Twee of meer tekens: Een 256-byte bool-tabel op de stack wordt gevuld vanuit de set, zolang de set-tekens in het bereik $1..0\text{xFF}$ liggen. Als een set-teken $\ge 0\text{x}100$ wordt gevonden, valt de functie terug op het aanroepen van wcschr() voor elk teken in de haystack. Correct.
  3. Overig: De haystack wordt gescand tegen de tabel:
  • a2564: ldrb w8, [x9, x8] → table[c]
  • a2568: tbnz w8, #0, a2578 → in set → stop (correct)
  • a256c: ldr w8, [x22, #0x4]! → volgend teken
  • a2570: cmp w8, #0x100
  • a2574: b.lo a2564 → $c < 0\text{x}100 \rightarrow$ scan voortzetten
  • a2578: ... → $c \ge 0\text{x}100 \rightarrow$ STOP, gerapporteerd als een match

Een teken $\ge 0\text{x}100$ kan niet in een set zitten waarvan alle leden $<$ U+0100 zijn. Voor wcscspn moet de scan daarom doorgaan. De code stopt echter. wcsspn heeft dezelfde structuur, maar daar is stoppen juist het correcte antwoord (omdat het teken niet in de keep-set zit), wat verklaart waarom alleen de complementaire functies foutief zijn. wcspbrk is geïmplementeerd bovenop deze scan en erft de bug over.

De bestaande bionic-test TEST(wchar, wcspbrk) in tests/wchartest.cpp gebruikt alleen ASCII-input en detecteert dit daarom niet. Een test zoals EXPECTEQ(3u, wcscspn(L"aĀb\n", L"x\n")) zou dit wel oppikken.

Impact in de praktijk

  • Visual Pinball on Android (libwinevbs): Sinds de september-update weigert deze Unix-build van de VBScript-engine van Wine tabellen te laden waarvan de broncode een commentaar met een emoji bevat. De lexer gebruikt wcspbrk(ptr, L"\n\r") om commentaren over te slaan, maar krijgt de positie van de eerste emoji in plaats van het einde van de regel, wat resulteert in een "Invalid character" melding.
  • FileSystem.MoveFolder: Behandelde elk pad met een teken $\ge$ U+0100 als een wildcard-patroon via wcspbrk(src, L"*?><").

Deze problemen zijn downstream opgelost door de aanroepen te vervangen door eenvoudige loops (zie vpinball/libwinevbs#41).

Analyse door ontwikkelaars

Google-collaborators hebben bevestigd dat dit gedrag correct is beschreven en dat het probleem nog steeds aanwezig is in de Tip of Tree (ToT).

Het is bevestigd dat wcspbrk() direct beïnvloed wordt omdat deze functie als volgt is geïmplementeerd:

__attribute__((__flatten__))
wchar_t* wcspbrk(const wchar_t* s, const wchar_t* delims) {
    size_t i = wcscspn(s, delims);
    if (s[i] != L'\0') return const_cast<wchar_t*>(s) + i;
    return nullptr;
}

Er is geconstateerd dat er een bestaande test is voor wcscspn(), maar dat deze onjuist is: wcscspn(L"he\ufffelo", L"lo") verwachtte 2u, maar gaf 3 terug.