File
Blob: src/workerd/api/tests/response-uaf-test.js
| 1 | // Copyright (c) 2026 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 a use-after-free: creating a Response from an ArrayBuffer, |
| 6 | // then transferring the buffer via structuredClone and collecting the clone, |
| 7 | // must not cause the Response body read to access freed memory. |
| 8 | // |
| 9 | // Without the fix, this crashes under ASAN with: |
| 10 | // heap-use-after-free READ of size 1024 |
| 11 | |
| 12 | export default { |
| 13 | async test() { |
| 14 | // Fixed-size ArrayBuffer (non-resizable). The UAF is not specific to |
| 15 | // resizable buffers — it affects any ArrayBuffer whose backing store can |
| 16 | // be transferred away. |
| 17 | const buffer = new ArrayBuffer(1024); |
| 18 | new Uint8Array(buffer).fill(0x42); |
| 19 | |
| 20 | const res = new Response(buffer); |
| 21 | |
| 22 | // Transfer detaches the original buffer. The clone becomes the sole |
| 23 | // JS-visible owner of the backing store. |
| 24 | structuredClone(buffer, { transfer: [buffer] }); |
| 25 | |
| 26 | // Collect the clone (which was not assigned to a variable). This frees |
| 27 | // the backing store memory if nothing else prevents it. |
| 28 | gc(); |
| 29 | |
| 30 | // Reading the body must not touch freed memory. |
| 31 | const result = new Uint8Array(await res.arrayBuffer()); |
| 32 | if (result.length !== 1024) { |
| 33 | throw new Error(`Expected 1024 bytes, got ${result.length}`); |
| 34 | } |
| 35 | // Verify the data is intact (not garbage from freed memory). |
| 36 | for (let i = 0; i < result.length; i++) { |
| 37 | if (result[i] !== 0x42) { |
| 38 | throw new Error( |
| 39 | `Byte ${i}: expected 0x42, got 0x${result[i].toString(16)}` |
| 40 | ); |
| 41 | } |
| 42 | } |
| 43 | }, |
| 44 | }; |