Skip to content

Workaround for a TextDecoder bug in Safari causing a RangeError to be thrown #4472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 6, 2025

Conversation

bes
Copy link
Contributor

@bes bes commented Apr 6, 2025

TextDecoder in Safari has a limitation that causes it to throw RangeError after decoding more than 2GiB of data. This causes long running wasm programs that need to use TextDecoder to crash and start throwing RuntimeError with the message "Out of bounds memory access".

We work around the issue by tracking how much data has been decoded by any given TextDecoder, and replace it when it comes close to 2GiB, deducting a small margin of 1MiB which has been empirically shown to reduce the likelihood of miscounting (for unknown reasons) causing a RangeError to be thrown.

This commit also adds stricter handling of the kind of declaration used for TextDecoder and TextEncoder - TextDecoder always uses let because it needs to be mutable, and TextEncoder always uses const because it doesn't need to be mutable.

Fixes #4471

@@ -11,16 +11,16 @@
(type (;9;) (func (param i32 f64)))
(import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0)))
(func $__wbindgen_realloc (;1;) (type 8) (param i32 i32 i32 i32) (result i32))
(func $__wbindgen_malloc (;2;) (type 6) (param i32 i32) (result i32))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why I needed these changes. I am using rustc 1.86.0, maybe I should be using another version?

@bes bes force-pushed the fix-safari-textdecoder-bug branch from 06346fa to 1a75ba6 Compare April 6, 2025 17:06
@bes
Copy link
Contributor Author

bes commented Apr 6, 2025

Some unrelated clippy lints are failing, has CI rustc been updated without updating the code?

@bes
Copy link
Contributor Author

bes commented Apr 7, 2025

There don't seem to be any tests (generated .js-files) that use the non-browser path of getText 🤔

@bes bes force-pushed the fix-safari-textdecoder-bug branch from 1a75ba6 to 20a27e6 Compare April 7, 2025 14:16
@stephanemagnenat
Copy link

Some unrelated clippy lints are failing, has CI rustc been updated without updating the code?

@daxpedda do you have any input on these? The PR works around a critical bug on Safari and would be very useful for many people!

@stephanemagnenat
Copy link

@bes I opened an issue (#4482) as even main does not compile any more in CI.

@bes
Copy link
Contributor Author

bes commented Apr 14, 2025

@bes I opened an issue (#4482) as even main does not compile any more in CI.

Thanks

@bes
Copy link
Contributor Author

bes commented Apr 15, 2025

We've been running this fix in production for a week now, and the results are encouraging. Screenshots from Sentry.

Ignore the first column, it's low because of a quirk in how Sentry does data cutoff...

range_error safari_by_version

Can you guess which day the fix went out?

@stephanemagnenat
Copy link

stephanemagnenat commented Aug 5, 2025

@bes the CI has been fixed in latest main commit, could you rebase your PR please? That should help it getting merged, now that there are more active maintainers (see #4533) ;-)

Copy link
Contributor

@daxpedda daxpedda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a tiny bit concerned about the 1MiB margin. This comment shows that it fails at 2,147,483,600 bytes and succeeds at 2,147,483,500 bytes. So the margin should be between 148 and 47 bytes.

Considering how reproducable this test looks like, it would be nice to find out the exact number it fails at. The precise number is actually not that important, it would just be nice to once and for all establish that it actually fails at a certain number and that its not flaky or anything.

In any case, until somebody finds the time to actually try this out we will leave it at the current 1MiB margin considering that there is real-world data behind it.

@bes bes force-pushed the fix-safari-textdecoder-bug branch from 20a27e6 to 056a294 Compare August 6, 2025 08:57
@bes
Copy link
Contributor Author

bes commented Aug 6, 2025

I'm a tiny bit concerned about the 1MiB margin. This comment shows that it fails at 2,147,483,600 bytes and succeeds at 2,147,483,500 bytes. So the margin should be between 148 and 47 bytes.

Considering how reproducable this test looks like, it would be nice to find out the exact number it fails at. The precise number is actually not that important, it would just be nice to once and for all establish that it actually fails at a certain number and that its not flaky or anything.

In any case, until somebody finds the time to actually try this out we will leave it at the current 1MiB margin considering that there is real-world data behind it.

I tried a small margin at first but could not get it to work on my machine, so I grabbed a margin which I thought was large enough to avoid problems but small enough to not have any significance (1 MiB is 0.049% of 2GiB)

@daxpedda
Copy link
Contributor

daxpedda commented Aug 6, 2025

I tried a small margin at first but could not get it to work on my machine, so I grabbed a margin which I thought was large enough to avoid problems but small enough to not have any significance (1 MiB is 0.049% of 2GiB)

If you can actually try it, could you do the following:

const decoder = new TextDecoder()
const buffer1 = new ArrayBuffer(100)

for (let i = 0; i < 21474836; i++) {
    decoder.decode(buffer1)
}

const buffer2 = new ArrayBuffer(1)
let i = 2147483601

try {
    for (; i <= 2147483648; i++) {
        decoder.decode(buffer2)
    }
} catch {
    console.err(`failed at ${i} number of bytes`)
}

And check if it fails consistently at the same number of bytes.

Copy link
Contributor

@daxpedda daxpedda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep forgetting this: please add an entry to the changelog as well!

… thrown

`TextDecoder` in Safari has a limitation that causes it to throw `RangeError`
after decoding more than 2GiB of data. This causes long running wasm programs
that need to use `TextDecoder` to crash and start throwing `RuntimeError` with
the message "Out of bounds memory access".

We work around the issue by tracking how much data has been decoded by any
given `TextDecoder`, and replace it when it comes close to 2GiB, deducting
a small margin of 1MiB which has been empirically shown to reduce the
likelihood of miscounting (for unknown reasons) causing a `RangeError` to
be thrown.

This commit also adds stricter handling of the kind of declaration used for
TextDecoder and TextEncoder - TextDecoder always uses let because it needs
to be mutable, and TextEncoder always uses const because it doesn't need to
be mutable.

Fixes wasm-bindgen#4471
@bes bes force-pushed the fix-safari-textdecoder-bug branch from 056a294 to e0b6124 Compare August 6, 2025 10:00
@bes
Copy link
Contributor Author

bes commented Aug 6, 2025

Added a line in the changelog.

If you can actually try it, could you do the following:

I'm on vacation right now, supposed to go on a day trip, so pushing these change kinda in between packing. I would appreciate if we can skip this for now :)

Copy link
Contributor

@daxpedda daxpedda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on vacation right now, supposed to go on a day trip, so pushing these change kinda in between packing. I would appreciate if we can skip this for now :)

Absolutely!

@daxpedda daxpedda merged commit f6a6c85 into wasm-bindgen:main Aug 6, 2025
@stephanemagnenat
Copy link

stephanemagnenat commented Aug 6, 2025

@bes thank you so much for your work!

@daxpedda thank you for merging! Any plan for making a release soon? This fix would be very useful to us!

@daxpedda
Copy link
Contributor

daxpedda commented Aug 6, 2025

I not yet back to maintaining wasm-bindgen so my time is quite limited right now, I'll leave this to the other maintainers to decide.

My 2¢: making a release is relatively low cost so I favor making one as soon as requested.

@stephanemagnenat
Copy link

stephanemagnenat commented Aug 6, 2025

My 2¢: making a release is relatively low cost so I favor making one as soon as requested.

Is there an ideal way to make such a request? Should I open a new issue for that, or do other maintainers read closed PR?

@daxpedda
Copy link
Contributor

daxpedda commented Aug 6, 2025

You asking for it here was the request.
But please open a tracking issue so it doesn't get lost!

@bes bes deleted the fix-safari-textdecoder-bug branch August 6, 2025 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Safari: TextDecoder refuses to process data after 2GB causing RangeError (Bad value) and Out of bounds memory access
3 participants