Skip to content
File

Blob: src/workerd/api/tests/streams-consumer-reentry-gc-test.js

javascript86 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// Regression test for EDGEWORKER-RUNTIME-H40: SIGSEGV during ConsumerImpl
6// close drain when re-entrant controller.error() destroys the consumer
7// mid-iteration.
8//
9// The mechanism: during maybeDrainAndSetState's close-drain loop, each
10// resolveAsDone call wraps ReadResult via wrapOpaque, which creates a V8
11// object. V8's promise resolution machinery checks for a "then" property
12// on the resolved value. A malicious Object.prototype.then getter can
13// re-enter C++ and call controller.error(), which:
14// 1) Rejects all remaining readRequests (error path in maybeDrainAndSetState)
15// 2) Transitions ConsumerImpl state to Errored (destroys Ready struct)
16// 3) Notifies stateListener -> doError -> destroys ValueReadable -> Consumer
17// The outer close-drain loop then dereferences freed memory -> SIGSEGV.
18//
19// The fix in queue.h extracts readRequests to local ownership before
20// resolving/rejecting and uses selfRef WeakRef to guard member access.
21 
22import { strictEqual } from 'node:assert';
23 
24export default {
25 async test() {
26 let controller;
27 const rs = new ReadableStream({
28 start(c) {
29 controller = c;
30 },
31 pull(c) {
32 return new Promise(() => {});
33 }, // never resolves -> reads stay pending
34 });
35 
36 // Let start() onSuccess microtask run.
37 await Promise.resolve();
38 
39 const reader = rs.getReader();
40 // Queue 3 pending readRequests in the consumer's RingBuffer.
41 reader.read().catch(() => {});
42 reader.read().catch(() => {});
43 reader.read().catch(() => {});
44 
45 let armed = false;
46 const noopThen = function (resolve, reject) {
47 /* never settle */
48 };
49 
50 // Install a trap on Object.prototype.then that re-enters the stream
51 // controller when V8 tries to resolve the first pending read.
52 // V8's promise resolution checks for a "then" property on the resolved
53 // value (thenable detection). If it finds one, it queues a microtask
54 // instead of immediately fulfilling. Our getter uses this window to
55 // call controller.error(), which re-enters ConsumerImpl and destroys
56 // the readRequests being iterated.
57 Object.defineProperty(Object.prototype, 'then', {
58 configurable: true,
59 get() {
60 if (armed) {
61 armed = false;
62 try {
63 controller.error(new Error('boom'));
64 } catch {
65 // Intentionally empty
66 }
67 return noopThen;
68 }
69 return undefined;
70 },
71 });
72 
73 armed = true;
74 try {
75 controller.close();
76 } catch {
77 // close may throw due to the re-entrant error — that's expected
78 }
79 armed = false;
80 delete Object.prototype.then;
81 
82 // If we get here without SIGSEGV, the fix works.
83 strictEqual(true, true, 'survived re-entrant error during close drain');
84 },
85};