[Pkg-javascript-devel] Bug#1142939: silent UTF-8 decoding corruption on s390x (code points U+XX00)
Xavier Guimard
yadd at debian.org
Tue Jul 28 12:57:48 BST 2026
Package: nodejs
Version: 24.18.0+dfsg+~cs24.13.2-1
Severity: important
X-Debbugs-Cc: debian-s390 at lists.debian.org
User: debian-s390 at lists.debian.org
Usertags: s390x
Control: affects -1 node-tar
On s390x (big endian), Buffer.prototype.toString('utf8') silently decodes a
code point as U+0000 when all of the following hold:
- the code point's low byte is zero (U+0100, U+2000, U+3000, U+4E00,
U+AC00, U+FB00, ... 230 code points in the BMP);
- it is at the very start of the buffer;
- it is followed only by NUL bytes;
- the buffer is at least 32 bytes long.
No exception is thrown, no U+FFFD is produced, and the decoded string has the
correct length -- only the code unit is wrong. Code points whose low byte is
non-zero are unaffected, which is why this went unnoticed.
$ node -e 'const b=Buffer.alloc(32); b.write("\u3000",0,"utf8");
console.log(b.toString("utf8").charCodeAt(0).toString(16))'
s390x: 0
amd64: 3000
Full matrix on s390x (amd64 is correct everywhere):
U+3000 + NUL*13 len=16 -> U+3000 ok
U+3000 + NUL*28 len=31 -> U+3000 ok
U+3000 + NUL*29 len=32 -> U+0000 BROKEN
U+3000 + NUL*64 len=67 -> U+0000 BROKEN
U+3000 + 'a'*29 len=32 -> U+3000 ok
'a'*29 + U+3000 + NUL*29 len=61 -> U+3000 ok
'a'*29 + U+3000, no NUL len=32 -> U+3000 ok
TextDecoder.decode() and Buffer.prototype.utf8Slice() are affected in the same
way, so the bug is in the shared UTF-8 decoding path.
simdutf has been ruled out: a native reproducer calling
simdutf::convert_utf8_to_utf16{,le,be} directly on the same input returns the
correct code unit for every length tested (active implementation "fallback",
libsimdutf 8.2.0-1). Sources and outputs of all reproducers attached.
The failure signature -- the low byte survives, the high byte is lost, and only
for code points whose low byte is zero -- suggests a byte-order confusion in a
"does this string fit in one byte (Latin-1)" fast path.
Impact: breaks node-tar test on s390x
https://ci.debian.net/packages/n/node-tar/testing/s390x/73799909/
Found after many tries with the help of Claude-Code
-------------- next part --------------
A non-text attachment was scrubbed...
Name: repro-node.js
Type: application/javascript
Size: 1161 bytes
Desc: not available
URL: <http://alioth-lists.debian.net/pipermail/pkg-javascript-devel/attachments/20260728/a3e20d35/attachment.js>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: repro2.js
Type: application/javascript
Size: 1304 bytes
Desc: not available
URL: <http://alioth-lists.debian.net/pipermail/pkg-javascript-devel/attachments/20260728/a3e20d35/attachment-0001.js>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: repro3.js
Type: application/javascript
Size: 1217 bytes
Desc: not available
URL: <http://alioth-lists.debian.net/pipermail/pkg-javascript-devel/attachments/20260728/a3e20d35/attachment-0002.js>
-------------- next part --------------
#include <simdutf.h>
#include <algorithm>
#include <cstdio>
#include <vector>
static void dump(const char *what, const char16_t *out, size_t n) {
const unsigned char *p = reinterpret_cast<const unsigned char *>(out);
printf(" %-6s n=%2zu unit0=0x%04X octets=%02X %02X\n",
what, n, (unsigned)out[0], p[0], p[1]);
}
int main() {
printf("implementation active : %s\n",
simdutf::get_active_implementation()->name().c_str());
for (size_t len : {16u, 31u, 32u, 64u}) {
std::vector<char> in(len, 0);
in[0] = (char)0xE3; in[1] = (char)0x80; in[2] = (char)0x80; // U+3000
std::vector<char16_t> out(len + 1);
printf(" len=%zu\n", len);
std::fill(out.begin(), out.end(), (char16_t)0xFFFF);
dump("native", out.data(),
simdutf::convert_utf8_to_utf16(in.data(), in.size(), out.data()));
std::fill(out.begin(), out.end(), (char16_t)0xFFFF);
dump("le", out.data(),
simdutf::convert_utf8_to_utf16le(in.data(), in.size(), out.data()));
std::fill(out.begin(), out.end(), (char16_t)0xFFFF);
dump("be", out.data(),
simdutf::convert_utf8_to_utf16be(in.data(), in.size(), out.data()));
}
return 0;
}
More information about the Pkg-javascript-devel
mailing list