#2027·PrivateBin

utf8To16() allocates ~3.4 GB to decode a 100 MB paste, killing the tab

Author: gpaugam-mvgCreated Sep 18, 2026Updated Sep 18, 2026
Labelscode qualityjavascript

CryptTool.utf8To16() (js/privatebin.js:1007, unchanged in master) decodes the decompressed paste like this:

javascript
decodeURIComponent(message.split('').map(c => '%' + hex(c)).join(''))

For a 78 MB attachment the input is 104,000,113 characters. That builds a 104M-element array of 1-char strings, then a second 104M-element array of newly allocated 3-char strings, then a 312M-character string.

Measured by calling utf8To16 alone on a string of that length, Chrome 149 headless, clean profile:

input peak JS heap time
104,000,000 chars 3,443 MB 10.1 s
109,333,336 chars 3,611 MB 10.5 s

Chrome caps the renderer heap at 4.40 GB and silently ignores --max-old-space-size above it, so there is no client-side workaround.

Steps to reproduce

  1. Upload a ~78 MB text file as an attachment
  2. Open the paste link

What happens

The tab dies with Out of memory. Traced on a self-hosted instance: decryption and inflate both complete (_freeInflateContext returns, ~334 MB live), then the JSON.parse of the decompressed result is never reached. The gap contains only utf8To16(arraybufferToString(data)).

The failure is not monotonic in file size, which makes it look random: 74, 82, 84, 86 and 90 MB succeed while 78 MB fails every time, reproduced with four different attachment contents (compressed payload ranging from 1.1 MB to 104 MB). That is simply where the allocations land relative to the cap.

Suggested fix

arraybufferToString() (:1049) + utf8To16() in decompress() (:1123) reimplement TextDecoder by hand. One pass, no intermediate arrays:

javascript
return new TextDecoder().decode(data);

The symmetric utf16To8() has the same shape and TextEncoder covers it.

Likely the undiagnosed cause of #562.