This fixes a race condition where async error handling tasks attempt to close
a codec that the user has already explicitly closed, causing an assertion
failure: MOZ_ASSERT(self->mState != CodecState::Closed).
The race occurs as follows:
An async decode/encode operation fails and queues a task to call CloseInternal()
Before that queued task runs, the user explicitly calls decoder.close()
The queued error task runs and hits the assertion
The assertions at call sites don't hold due to this race condition. The fix
removes these assertions and instead adds a check in CloseInternal() to return
early if already closed, making close operations idempotent.
Per the WebCodecs spec, the Close algorithm calls Reset, and Reset is specified
to throw InvalidStateError if the state is already "closed". Our ResetInternal()
implements this check (line 283-285). By checking in CloseInternal() first, we
avoid calling Reset on an already-closed codec, which respects the spec's
invariant while handling the race gracefully.
This approach is spec-compliant because:
It respects the invariant that Reset should not operate on closed codecs
Close operations are inherently idempotent (closing an already-closed codec is a no-op, as the codec is already in the terminal "closed" state)
It prevents error callbacks from firing on codecs the user has already closed
Note: This race is extremely rare and timing-dependent. Attempts to reproduce
included: 10 normal runs, 20 runs with stress-ng (--cpu 4 --vm 2), and 20 runs
with MOZ_CHAOSMODE=0xfb. None triggered the assertion. The fuzzer found it,
confirming it exists, but the race window is exceptionally narrow. I still think
the fix is correct but I can't prove it.
Differential Revision: https://phabricator.services.mozilla.com/D282422
31 lines
553 B
HTML
31 lines
553 B
HTML
<!DOCTYPE html>
|
|
<html class="reftest-wait">
|
|
<body>
|
|
<script>
|
|
if (typeof VideoDecoder !== 'undefined') {
|
|
const decoder = new VideoDecoder({
|
|
output: frame => frame.close(),
|
|
error: e => {}
|
|
});
|
|
|
|
decoder.configure({
|
|
codec: 'vp8',
|
|
codedWidth: 320,
|
|
codedHeight: 240,
|
|
});
|
|
|
|
const invalidData = new Uint8Array(100);
|
|
const chunk = new EncodedVideoChunk({
|
|
type: 'key',
|
|
timestamp: 0,
|
|
data: invalidData,
|
|
});
|
|
|
|
decoder.decode(chunk);
|
|
decoder.close();
|
|
}
|
|
document.documentElement.className = "";
|
|
</script>
|
|
</body>
|
|
</html>
|