[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