Skip to content

Commit 4f6224c

Browse files
fix: Bound async state serialization so a blocked thread can't freeze or crash
SanitizeForJSON walked the AsyncLocalStorage state without any limits while the captured thread was interrupted. Shared references were copied once per path, linked structures (e.g. Node timer lists reachable from a Sentry client) recursed once per node with an O(depth) cycle check, and exceptions from the walk escaped into the interrupted JavaScript. - Limit depth (10), entries per container (1000), values read (20000) and string length (1 MiB); cut data is replaced by visible markers - Copy own enumerable string keys only, into null-prototype containers so inherited toJSON hooks can't bypass the limits - Replace Proxies by "[Proxy]" without running traps - Catch exceptions during state serialization and keep V8 execution termination
1 parent 78e7c4a commit 4f6224c

25 files changed

Lines changed: 1235 additions & 83 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Bug Fixes 🐛
6+
7+
- Limit the serialization of `asyncState` and `pollState` (depth, properties
8+
per object, total reads and total string length, with markers such as
9+
`"[Object]"` and `"[MaxProperties ~]"` where data was cut), so capturing a
10+
stack trace no longer freezes, crashes or runs out of memory on large or
11+
deeply linked state. Exceptions thrown while reading the state no longer
12+
reach the captured thread, only own properties are serialized, and `Proxy`
13+
values become `"[Proxy]"`.
14+
315
## 0.5.2
416

517
### Bug Fixes 🐛

‎README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,37 @@ Sends a heartbeat from the current thread.
233233

234234
Returns the time in milliseconds since each registered thread called
235235
`threadPoll()`.
236+
237+
### State serialization
238+
239+
`asyncState` and `pollState` are best-effort, size-limited snapshots.
240+
`asyncState` is read on the captured thread while its JavaScript is
241+
interrupted, so the serialization is limited whatever the state contains:
242+
243+
- Values are copied like `JSON.stringify` copies them: own enumerable
244+
string-keyed properties only, while functions, symbols, BigInts, `undefined`
245+
and circular references are left out. Shared references that are not circular
246+
are copied each time they appear. Integer-like keys of plain objects (e.g.
247+
`"0"`) are left out, and `toJSON` methods are not called.
248+
- Getters run, as they do with `JSON.stringify`. A property whose getter throws
249+
is left out. Exceptions never reach the code running on the captured thread,
250+
but termination (e.g. `worker.terminate()`) still takes effect. A `Proxy`
251+
becomes `"[Proxy]"` without running its traps.
252+
- Limits, with markers replacing what was cut:
253+
- Depth 10: objects and arrays nested deeper become `"[Object]"` or
254+
`"[Array]"`.
255+
- 1,000 properties or elements per object or array: the rest is replaced by a
256+
single `"[MaxProperties ~]"` entry (the next element of an array, or the
257+
value of the first key left out of an object).
258+
- 20,000 property or element reads in total: after that nothing more is read.
259+
The objects and arrays being copied end with a `"[MaxProperties ~]"` entry,
260+
and later objects and arrays become `"[Object]"` or `"[Array]"`.
261+
- 1,048,576 characters (UTF-16 code units) of strings in total, counting
262+
values and keys: the string that crosses the limit is cut and ends with
263+
`"..."`, and later strings become `"[String]"`. An object whose next key
264+
does not fit ends with a `"...": "[MaxProperties ~]"` entry.
265+
- The limits bound the size of the snapshot and how deep the serialization
266+
recurses. The serialization still reads the full list of own keys of each
267+
object it copies, and still runs getters, so a slow getter or a very wide
268+
object delays the snapshot.
269+
- If the state cannot be read at all, `asyncState` is left out.

0 commit comments

Comments
 (0)