File
Blob: src/workerd/api/system-streams-test.c++
| 1 | // Copyright (c) 2017-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 | #include "system-streams.h" |
| 6 | |
| 7 | #include <workerd/io/io-context.h> |
| 8 | #include <workerd/tests/test-fixture.h> |
| 9 | |
| 10 | #include <kj/test.h> |
| 11 | |
| 12 | namespace workerd::api { |
| 13 | namespace { |
| 14 | |
| 15 | KJ_TEST("EncodedAsyncInputStream cancel with pending read on AsyncPipe") { |
| 16 | // This test reproduces a use-after-free crash that occurred when: |
| 17 | // 1. A read operation is started on an EncodedAsyncInputStream backed by an AsyncPipe |
| 18 | // 2. The stream is cancelled (e.g., via Socket::close()) |
| 19 | // 3. The AsyncPipe is destroyed while the read is still pending |
| 20 | // |
| 21 | // Without the fix (kj::Canceler in EncodedAsyncInputStream), the BlockedRead destructor |
| 22 | // would try to access the freed AsyncPipe, causing a use-after-free. |
| 23 | |
| 24 | TestFixture fixture; |
| 25 | fixture.runInIoContext([](const TestFixture::Environment& env) -> kj::Promise<void> { |
| 26 | // Create an in-memory pipe (AsyncPipe) |
| 27 | auto pipe = kj::newTwoWayPipe(); |
| 28 | |
| 29 | // Create an EncodedAsyncInputStream wrapping one end of the pipe |
| 30 | kj::Own<kj::AsyncInputStream> inputStream = kj::mv(pipe.ends[0]); |
| 31 | auto stream = newSystemStream(kj::mv(inputStream), StreamEncoding::IDENTITY, env.context); |
| 32 | |
| 33 | // Start a read operation - this will block because no data has been written to the pipe |
| 34 | kj::byte buffer[100]; |
| 35 | auto readPromise = stream->tryRead(buffer, 1, sizeof(buffer)); |
| 36 | |
| 37 | // Cancel the stream - this simulates what Socket::close() does |
| 38 | stream->cancel(KJ_EXCEPTION(DISCONNECTED, "stream cancelled")); |
| 39 | |
| 40 | // Now destroy the other end of the pipe - this destroys the AsyncPipe |
| 41 | // Without the fix, this would cause a use-after-free when the BlockedRead |
| 42 | // destructor tries to access the freed pipe. |
| 43 | pipe.ends[1] = nullptr; |
| 44 | |
| 45 | // The read promise should be cancelled - try to wait for it |
| 46 | // It should reject with the cancellation exception |
| 47 | return readPromise.then( |
| 48 | [](size_t) { KJ_FAIL_ASSERT("read should have been cancelled"); }, [](kj::Exception&& e) { |
| 49 | // Expected the read to be cancelled |
| 50 | }); |
| 51 | }); |
| 52 | } |
| 53 | |
| 54 | } // namespace |
| 55 | } // namespace workerd::api |