wcscspn()/wcspbrk() deal with each wchar_t >= 0x100 as a match (Android 17, CP3A.260905.009) · Difficulty #2260 · android/ndk · GitHub

Description
On Android 17 builds from September 2026, wcscspn() and wcspbrk() cease on the first character of the string that’s >= U+0100 at any time when the reject set has two or extra characters which can be all < U+0100. The character is reported as a match although it isn’t within the set. wcsspn(), wcschr(), wcsrchr(), wmemchr() and wcsstr() are appropriate.
Any code that scans broad strings for ASCII delimiters, for instance newline or wildcard characters, breaks as quickly because the textual content comprises an emoji, CJK, Cyrillic, Greek or Latin Extended character.
Affected / unaffected
| Device | Build | Result |
|---|---|---|
| Pixel 7 | google/panther/panther:17/CP3A.260905.009/16091614:person/release-keys | damaged |
| Pixel 8a | google/akita/akita:17/CP3A.260905.009/16091614:person/release-keys (patch 2026-09-05) | damaged |
| Nothing Phone 2 | Nothing/PongEEA/Pong:16/BQ2A.250721.001-BP2A.250605.031.A3 (patch 2026-08-01) | okay |
The public android17-release sources (android-17.0.0_r1, June 2026) construct wcscspn/wcspbrk from exterior/llvm-libc, which is a plain nested loop over wchar_t and is appropriate. The code on the September builds is completely different (see “Mechanism”), so the regression was launched between the June and September releases.
Reproducer
Build with the NDK (aarch64-linux-android30-clang), push to /information/native/tmp, run with adb shell:
#embody
#embody
int principal(void)
{
wchar_t s[] = {'a', 0x1F680, 0x100, 'b', 'x', 'n', 0};
printf("wcscspn(s, L"xn") = %zu (anticipated 4)n", wcscspn(s, L"xn"));
printf("wcspbrk(s, L"xn") = index %ld (anticipated 4)n", (lengthy)(wcspbrk(s, L"xn") - s));
printf("wcsspn(s, L"aU0001F680") = %zu (anticipated 2)n", wcsspn(s, L"aU0001F680"));
return 0;
}
Output on CP3A.260905.009:
wcscspn(s, L"xn") = 1 (anticipated 4)
wcspbrk(s, L"xn") = index 1 (anticipated 4)
wcsspn(s, L"aU0001F680") = 2 (anticipated 2)
A sweep of each code level from U+0001 to U+10FFFF (skipping surrogates and the 2 set members) with wcscspn(L" returns 0 as a substitute of 1 for 1,111,808 code factors: each one from U+0100 to U+10FFFF. U+0001 to U+00FF are appropriate.
Which set shapes set off it (all measured on the Pixel 8a):
| Reject set | Result |
|---|---|
| one character | okay |
two or extra characters, all < U+0100 (e.g. L"xn", L"xe9n") |
damaged |
| two or extra characters, no less than one >= U+0100 | okay |
| haystack restricted to U+0080..U+00FF | okay |
Mechanism (from the shipped libc.so)
Disassembly of wcscspn in /apex/com.android.runtime/lib64/bionic/libc.so on CP3A.260905.009 reveals three paths chosen by the set:
- One character: a plain evaluate loop. Correct.
- Two or extra characters: a 256-byte
booldesk on the stack is stuffed from the set whereas the set characters are in 1..0xFF. If a set character >= 0x100 is seen, the perform falls again to callingwcschr()for every haystack character. Correct. - Otherwise the haystack is scanned towards the desk:
a2564: ldrb w8, [x9, x8] ; desk[c]
a2568: tbnz w8, #0, a2578 ; in set -> cease (appropriate)
a256c: ldr w8, [x22, #0x4]! ; subsequent character
a2570: cmp w8, #0x100
a2574: b.lo a2564 ; c < 0x100 -> hold scanning
a2578: ... ; c >= 0x100 -> STOP, reported as a match
A personality >= 0x100 can’t be in a set whose members are all < 0x100, so for wcscspn the scan should proceed previous it. The code stops as a substitute. wcsspn has the identical construction, and there stopping is the right reply (the character just isn’t within the hold set), which is why solely the complement capabilities are mistaken. wcspbrk is carried out on high of this scan and inherits the bug.
The current bionic take a look at TEST(wchar, wcspbrk) in assessments/wchar_test.cpp solely makes use of ASCII enter, so it doesn’t catch this. A take a look at comparable to EXPECT_EQ(3u, wcscspn(L"aĀbn", L"xn")) would.
Real-world affect
Visual Pinball on Android (libwinevbs, a Unix construct of Wine’s vbscript engine) stopped loading any desk whose VBScript supply comprises a remark with an emoji after the September replace: the lexer makes use of wcspbrk(ptr, L"nr") to skip feedback, bought the place of the primary emoji as a substitute of the road finish, and reported “Invalid character”. FileSystem.MoveFolder likewise handled each path containing a personality >= U+0100 as a wildcard sample through wcspbrk(src, L"*?><").
Worked round downstream by changing the calls with plain loops: vpinball/libwinevbs#41 (shipped in vpinball/vpinball@e7837322f4).
Full disassembly of the affected capabilities and the probe sources can be found on request.
I’m utilizing a supported NDK
Affected variations
r29
