LibWeb: Widen assertion to avoid accidentally writing to a closed stream

This widens the assertion from only checking if the WritableStream's
state is Errored or Erroring to asserting that the WritableStream is not
in a Writable state.
This commit is contained in:
Kenneth Myhra 2025-04-14 19:21:12 +02:00 committed by Tim Flynn
parent 6ba60188b4
commit b285202951
Notes: github-actions[bot] 2025-04-14 18:51:59 +00:00
4 changed files with 530 additions and 2 deletions

View file

@ -4974,8 +4974,8 @@ JS::Value writable_stream_default_controller_get_chunk_size(WritableStreamDefaul
{
// 1. If controller.[[strategySizeAlgorithm]] is undefined, then:
if (!controller.strategy_size_algorithm()) {
// 1. Assert: controller.[[stream]].[[state]] is "erroring" or "errored".
VERIFY(controller.stream()->state() == WritableStream::State::Erroring || controller.stream()->state() == WritableStream::State::Errored);
// 1. Assert: controller.[[stream]].[[state]] is not "writable".
VERIFY(controller.stream()->state() != WritableStream::State::Writable);
// 2. Return 1.
return JS::Value { 1.0 };

View file

@ -0,0 +1,31 @@
Harness status: OK
Found 26 tests
26 Pass
Pass fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined value
Pass when sink calls error asynchronously while sink close is in-flight, the stream should not become errored
Pass when sink calls error synchronously while closing, the stream should not become errored
Pass when the sink throws during close, and the close is requested while a write is still in-flight, the stream should become errored during the close
Pass releaseLock on a stream with a pending write in which the stream has been errored
Pass releaseLock on a stream with a pending close in which controller.error() was called
Pass when close is called on a WritableStream in writable state, ready should return a fulfilled promise
Pass when close is called on a WritableStream in waiting state, ready promise should be fulfilled
Pass when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close takes a long time
Pass returning a thenable from close() should work
Pass releaseLock() should not change the result of sync close()
Pass releaseLock() should not change the result of async close()
Pass close() should set state to CLOSED even if writer has detached
Pass the promise returned by async abort during close should resolve
Pass promises must fulfill/reject in the expected order on closure
Pass promises must fulfill/reject in the expected order on aborted closure
Pass promises must fulfill/reject in the expected order on aborted and errored closure
Pass close() should not reject until no sink methods are in flight
Pass ready promise should be initialised as fulfilled for a writer on a closed stream
Pass close() on a writable stream should work
Pass close() on a locked stream should reject
Pass close() on an erroring stream should reject
Pass close() on an errored stream should reject
Pass close() on an closed stream should reject
Pass close() on a stream with a pending close should reject
Pass write() on a closed stream should reject

View file

@ -0,0 +1,16 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/test-utils.js"></script>
<script src="../resources/recording-streams.js"></script>
<div id=log></div>
<script src="../../streams/writable-streams/close.any.js"></script>

View file

@ -0,0 +1,481 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
// META: script=../resources/recording-streams.js
'use strict';
const error1 = new Error('error1');
error1.name = 'error1';
const error2 = new Error('error2');
error2.name = 'error2';
promise_test(() => {
const ws = new WritableStream({
close() {
return 'Hello';
}
});
const writer = ws.getWriter();
const closePromise = writer.close();
return closePromise.then(value => assert_equals(value, undefined, 'fulfillment value must be undefined'));
}, 'fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined ' +
'value');
promise_test(() => {
let controller;
let resolveClose;
const ws = new WritableStream({
start(c) {
controller = c;
},
close() {
return new Promise(resolve => {
resolveClose = resolve;
});
}
});
const writer = ws.getWriter();
const closePromise = writer.close();
return flushAsyncEvents().then(() => {
controller.error(error1);
return flushAsyncEvents();
}).then(() => {
resolveClose();
return Promise.all([
closePromise,
writer.closed,
flushAsyncEvents().then(() => writer.closed)]);
});
}, 'when sink calls error asynchronously while sink close is in-flight, the stream should not become errored');
promise_test(() => {
let controller;
const passedError = new Error('error me');
const ws = new WritableStream({
start(c) {
controller = c;
},
close() {
controller.error(passedError);
}
});
const writer = ws.getWriter();
return writer.close().then(() => writer.closed);
}, 'when sink calls error synchronously while closing, the stream should not become errored');
promise_test(t => {
const ws = new WritableStream({
close() {
throw error1;
}
});
const writer = ws.getWriter();
return Promise.all([
writer.write('y'),
promise_rejects_exactly(t, error1, writer.close(), 'close() must reject with the error'),
promise_rejects_exactly(t, error1, writer.closed, 'closed must reject with the error')
]);
}, 'when the sink throws during close, and the close is requested while a write is still in-flight, the stream should ' +
'become errored during the close');
promise_test(() => {
const ws = new WritableStream({
write(chunk, controller) {
controller.error(error1);
return new Promise(() => {});
}
});
const writer = ws.getWriter();
writer.write('a');
return delay(0).then(() => {
writer.releaseLock();
});
}, 'releaseLock on a stream with a pending write in which the stream has been errored');
promise_test(() => {
let controller;
const ws = new WritableStream({
start(c) {
controller = c;
},
close() {
controller.error(error1);
return new Promise(() => {});
}
});
const writer = ws.getWriter();
writer.close();
return delay(0).then(() => {
writer.releaseLock();
});
}, 'releaseLock on a stream with a pending close in which controller.error() was called');
promise_test(() => {
const ws = recordingWritableStream();
const writer = ws.getWriter();
return writer.ready.then(() => {
assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
writer.close();
assert_equals(writer.desiredSize, 1, 'desiredSize should be still 1');
return writer.ready.then(v => {
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
assert_array_equals(ws.events, ['close'], 'write and abort should not be called');
});
});
}, 'when close is called on a WritableStream in writable state, ready should return a fulfilled promise');
promise_test(() => {
const ws = recordingWritableStream({
write() {
return new Promise(() => {});
}
});
const writer = ws.getWriter();
return writer.ready.then(() => {
writer.write('a');
assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
let calledClose = false;
return Promise.all([
writer.ready.then(v => {
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
assert_true(calledClose, 'ready should not be fulfilled before writer.close() is called');
assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called');
}),
flushAsyncEvents().then(() => {
writer.close();
calledClose = true;
})
]);
});
}, 'when close is called on a WritableStream in waiting state, ready promise should be fulfilled');
promise_test(() => {
let asyncCloseFinished = false;
const ws = recordingWritableStream({
close() {
return flushAsyncEvents().then(() => {
asyncCloseFinished = true;
});
}
});
const writer = ws.getWriter();
return writer.ready.then(() => {
writer.write('a');
writer.close();
return writer.ready.then(v => {
assert_false(asyncCloseFinished, 'ready promise should be fulfilled before async close completes');
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
assert_array_equals(ws.events, ['write', 'a', 'close'], 'sink abort() should not be called');
});
});
}, 'when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close ' +
'takes a long time');
promise_test(t => {
const rejection = { name: 'letter' };
const ws = new WritableStream({
close() {
return {
then(onFulfilled, onRejected) { onRejected(rejection); }
};
}
});
return promise_rejects_exactly(t, rejection, ws.getWriter().close(), 'close() should return a rejection');
}, 'returning a thenable from close() should work');
promise_test(t => {
const ws = new WritableStream();
const writer = ws.getWriter();
return writer.ready.then(() => {
const closePromise = writer.close();
const closedPromise = writer.closed;
writer.releaseLock();
return Promise.all([
closePromise,
promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
]);
});
}, 'releaseLock() should not change the result of sync close()');
promise_test(t => {
const ws = new WritableStream({
close() {
return flushAsyncEvents();
}
});
const writer = ws.getWriter();
return writer.ready.then(() => {
const closePromise = writer.close();
const closedPromise = writer.closed;
writer.releaseLock();
return Promise.all([
closePromise,
promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
]);
});
}, 'releaseLock() should not change the result of async close()');
promise_test(() => {
let resolveClose;
const ws = new WritableStream({
close() {
const promise = new Promise(resolve => {
resolveClose = resolve;
});
return promise;
}
});
const writer = ws.getWriter();
const closePromise = writer.close();
writer.releaseLock();
return delay(0).then(() => {
resolveClose();
return closePromise.then(() => {
assert_equals(ws.getWriter().desiredSize, 0, 'desiredSize should be 0');
});
});
}, 'close() should set state to CLOSED even if writer has detached');
promise_test(() => {
let resolveClose;
const ws = new WritableStream({
close() {
const promise = new Promise(resolve => {
resolveClose = resolve;
});
return promise;
}
});
const writer = ws.getWriter();
writer.close();
writer.releaseLock();
return delay(0).then(() => {
const abortingWriter = ws.getWriter();
const abortPromise = abortingWriter.abort();
abortingWriter.releaseLock();
resolveClose();
return abortPromise;
});
}, 'the promise returned by async abort during close should resolve');
// Though the order in which the promises are fulfilled or rejected is arbitrary, we're checking it for
// interoperability. We can change the order as long as we file bugs on all implementers to update to the latest tests
// to keep them interoperable.
promise_test(() => {
const ws = new WritableStream({});
const writer = ws.getWriter();
const closePromise = writer.close();
const events = [];
return Promise.all([
closePromise.then(() => {
events.push('closePromise');
}),
writer.closed.then(() => {
events.push('closed');
})
]).then(() => {
assert_array_equals(events, ['closePromise', 'closed'],
'promises must fulfill/reject in the expected order');
});
}, 'promises must fulfill/reject in the expected order on closure');
promise_test(() => {
const ws = new WritableStream({});
// Wait until the WritableStream starts so that the close() call gets processed. Otherwise, abort() will be
// processed without waiting for completion of the close().
return delay(0).then(() => {
const writer = ws.getWriter();
const closePromise = writer.close();
const abortPromise = writer.abort(error1);
const events = [];
return Promise.all([
closePromise.then(() => {
events.push('closePromise');
}),
abortPromise.then(() => {
events.push('abortPromise');
}),
writer.closed.then(() => {
events.push('closed');
})
]).then(() => {
assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
'promises must fulfill/reject in the expected order');
});
});
}, 'promises must fulfill/reject in the expected order on aborted closure');
promise_test(t => {
const ws = new WritableStream({
close() {
return Promise.reject(error1);
}
});
// Wait until the WritableStream starts so that the close() call gets processed.
return delay(0).then(() => {
const writer = ws.getWriter();
const closePromise = writer.close();
const abortPromise = writer.abort(error2);
const events = [];
closePromise.catch(() => events.push('closePromise'));
abortPromise.catch(() => events.push('abortPromise'));
writer.closed.catch(() => events.push('closed'));
return Promise.all([
promise_rejects_exactly(t, error1, closePromise,
'closePromise must reject with the error returned from the sink\'s close method'),
promise_rejects_exactly(t, error1, abortPromise,
'abortPromise must reject with the error returned from the sink\'s close method'),
promise_rejects_exactly(t, error2, writer.closed,
'writer.closed must reject with error2')
]).then(() => {
assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
'promises must fulfill/reject in the expected order');
});
});
}, 'promises must fulfill/reject in the expected order on aborted and errored closure');
promise_test(t => {
let resolveWrite;
let controller;
const ws = new WritableStream({
write(chunk, c) {
controller = c;
return new Promise(resolve => {
resolveWrite = resolve;
});
}
});
const writer = ws.getWriter();
return writer.ready.then(() => {
const writePromise = writer.write('c');
controller.error(error1);
const closePromise = writer.close();
let closeRejected = false;
closePromise.catch(() => {
closeRejected = true;
});
return flushAsyncEvents().then(() => {
assert_false(closeRejected);
resolveWrite();
return Promise.all([
writePromise,
promise_rejects_exactly(t, error1, closePromise, 'close() should reject')
]).then(() => {
assert_true(closeRejected);
});
});
});
}, 'close() should not reject until no sink methods are in flight');
promise_test(() => {
const ws = new WritableStream();
const writer1 = ws.getWriter();
return writer1.close().then(() => {
writer1.releaseLock();
const writer2 = ws.getWriter();
const ready = writer2.ready;
assert_equals(ready.constructor, Promise);
return ready;
});
}, 'ready promise should be initialised as fulfilled for a writer on a closed stream');
promise_test(() => {
const ws = new WritableStream();
ws.close();
const writer = ws.getWriter();
return writer.closed;
}, 'close() on a writable stream should work');
promise_test(t => {
const ws = new WritableStream();
ws.getWriter();
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
}, 'close() on a locked stream should reject');
promise_test(t => {
const ws = new WritableStream({
start(controller) {
controller.error(error1);
}
});
return promise_rejects_exactly(t, error1, ws.close(), 'close should reject with error1');
}, 'close() on an erroring stream should reject');
promise_test(t => {
const ws = new WritableStream({
start(controller) {
controller.error(error1);
}
});
const writer = ws.getWriter();
return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the error').then(() => {
writer.releaseLock();
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
});
}, 'close() on an errored stream should reject');
promise_test(t => {
const ws = new WritableStream();
const writer = ws.getWriter();
return writer.close().then(() => {
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
});
}, 'close() on an closed stream should reject');
promise_test(t => {
const ws = new WritableStream({
close() {
return new Promise(() => {});
}
});
const writer = ws.getWriter();
writer.close();
writer.releaseLock();
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
}, 'close() on a stream with a pending close should reject');
// See https://github.com/whatwg/streams/issues/1341.
promise_test(async t => {
const ws = new WritableStream();
const writer = ws.getWriter();
await writer.write(1);
await writer.close();
return promise_rejects_js(t, TypeError, writer.write(2), 'write should reject');
}, 'write() on a closed stream should reject');