Description
On Android 17 builds from September 2026, wcscspn() and wcspbrk() halt at the archetypal character of the cord that is >= U+0100 whenever the refuse set has two or additional characters that are all < U+0100. The character is reported as a equivalent equal although it is not in the set. wcsspn(), wcschr(), wcsrchr(), wmemchr() and wcsstr() are correct.
Any code that scans broad strings for ASCII delimiters, for example newline or wildcard characters, breaks as shortly as the content contains an emoji, CJK, Cyrillic, Greek or Latin Extended character.
Affected / unaffected
| Device | Build | Result |
|---|---|---|
| Pixel 7 | google/panther/panther:17/CP3A.260905.009/16091614:user/release-keys | broken |
| Pixel 8a | google/akita/akita:17/CP3A.260905.009/16091614:user/release-keys (patch 2026-09-05) | broken |
| Nothing Phone 2 | Nothing/PongEEA/Pong:16/BQ2A.250721.001-BP2A.250605.031.A3 (patch 2026-08-01) | ok |
The community android17-release sources (android-17.0.0_r1, June 2026) build wcscspn/wcspbrk from external/llvm-libc, which is a plain nested iteration complete wchar_t and is correct. The code on the September builds is distinct (see "Mechanism"), so the regression was introduced between the June and September releases.
Reproducer
Build alongside the NDK (aarch64-linux-android30-clang), shove to /data/local/tmp, run alongside 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\") = indicator %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 on CP3A.260905.009:
wcscspn(s, L"x\n") = 1 (expected 4) wcspbrk(s, L"x\n") = indicator 1 (expected 4) wcsspn(s, L"a\U0001F680") = 2 (expected 2)
A clear of all code item from U+0001 to U+10FFFF (skipping surrogates and the two set members) alongside wcscspn(L"<cp>\n", L"x\n") returns 0 alternatively of 1 for 1,111,808 code points: all one from U+0100 to U+10FFFF. U+0001 to U+00FF are correct.
Which set shapes trigger it (all measured on the Pixel 8a):
| Reject set | Result |
|---|---|
| one character | ok |
| two or additional characters, all < U+0100 (e.g. L"x\n", L"\xe9\n") | broken |
| two or additional characters, at smallest one >= U+0100 | ok |
| haystack constricted to U+0080..U+00FF | ok |
Mechanism (from the shipped libc.so)
Disassembly of wcscspn in /apex/com.android.runtime/lib64/bionic/libc.so on CP3A.260905.009 shows three paths selected by the set:
- One character: a plain difference loop. Correct.
- Two or additional characters: a 256-byte bool array on the stack is filled from the set during the set characters are in 1..0xFF. If a set character >= 0x100 is seen, the function falls rear to calling wcschr() for all haystack character. Correct.
- Otherwise the haystack is scanned against the table:
a2564: ldrb w8, [x9, x8] ; table[c] a2568: tbnz w8, #0, a2578 ; in set -> halt (correct) a256c: ldr w8, [x22, #0x4]! ; next character a2570: cmp w8, #0x100 a2574: b.lo a2564 ; c < 0x100 -> keep scanning a2578: ... ; c >= 0x100 -> STOP, reported as a match
A character >= 0x100 cannot be in a set whose members are all < 0x100, so for wcscspn the scan must continue former it. The code stops instead. wcsspn has the identical structure, and there stopping is the accurate answer (the character is not in the keep set), which is why lone the complement functions are wrong. wcspbrk is implemented on top of this scan and inherits the bug.
The existing bionic test TEST(wchar, wcspbrk) in tests/wchar_test.cpp lone uses ASCII input, so it does not capture this. A test specified as EXPECT_EQ(3u, wcscspn(L"aĀb\n", L"x\n")) would.
Real-world impact
Visual Pinball on Android (libwinevbs, a Unix build of Wine's vbscript engine) stopped loading any array whose VBScript origin contains a comment alongside an emoji following the September update: the lexer uses wcspbrk(ptr, L"\n\r") to skip comments, got the stance of the archetypal emoji alternatively of the row end, and reported "Invalid character". FileSystem.MoveFolder likewise treated all way containing a character >= U+0100 as a wildcard form via wcspbrk(src, L"*?><").
Worked about downstream by replacing the calls alongside plain loops: vpinball/libwinevbs#41 (shipped in vpinball/vpinball@e7837322f4).
Full disassembly of the affected functions and the investigation sources are accessible on request.
I am using a supported NDK
- I have checked and the NDK I'm using is currently supported
Affected versions
r29