webapi: reject writes to detached buffers in WebCodecs

This commit is contained in:
Halil Durak committed 2026-10-27 20:34:54 +03:00
1 parent 92eb451400
commit 98f4df5b43
4 files changed
+52 -2

No files matched your search

@@ -87,3 +87,27 @@
}
}
</script>
<script id=copy_to_detached_destination>
{
// An options getter that detaches the destination must not cause a write
// into the freed backing store: copyTo re-reads the destination after the
// options are coerced, so a detached buffer fails the bounds check instead.
const src = new AudioData({
format: 'u8', sampleRate: 8000, numberOfFrames: 8,
numberOfChannels: 1, timestamp: 0, data: new Uint8Array(8),
});
const dest = new Uint8Array(8);
let threw = false;
try {
src.copyTo(dest, {
planeIndex: 0,
get frameOffset() { dest.buffer.transfer(0); return 0; },
});
} catch (e) {
threw = true;
}
testing.expectEqual(true, threw);
testing.expectEqual(0, dest.byteLength);
}
</script>
@@ -143,3 +143,24 @@
testing.expectError('InvalidStateError', () => new VideoFrame(new OffscreenCanvas(32767, 32767), {timestamp: 1}));
}
</script>
<script id=copy_to_detached_destination type=module>
const state = await testing.async();
// An options getter that detaches the destination must not cause a write
// into the freed backing store: copyTo re-reads the destination after the
// options are coerced, so a detached buffer rejects instead.
const frame = new VideoFrame(new Uint8Array([1, 2, 3, 4]), {format: 'RGBA', timestamp: 0, codedWidth: 1, codedHeight: 1});
const dest = new Uint8Array(4);
let error = null;
try {
await frame.copyTo(dest, {get rect() { dest.buffer.transfer(0); return undefined; }});
} catch (e) {
error = e;
}
state.resolve();
await state.done(() => {
testing.expectEqual(true, error !== null);
testing.expectEqual(0, dest.byteLength);
});
</script>
+3 -1
View File
@@ -259,7 +259,9 @@ fn allocationSize(self: *const AudioData, options: CopyToOptions) !u32 {
return std.math.cast(u32, copy.byteLength()) orelse error.RangeError;
}
fn copyTo(self: *const AudioData, destination: js.AllowSharedBufferSource, options: CopyToOptions) !void {
fn copyTo(self: *const AudioData, destination_value: js.Value, options: CopyToOptions, exec: *const Execution) !void {
const destination = try exec.js.local.?.jsValueToZig(js.AllowSharedBufferSource, destination_value);
const copy = try self.computeCopy(options);
const byte_length = copy.byteLength();
if (byte_length > destination.bytes.len) {
+4 -1
View File
@@ -745,8 +745,11 @@ fn allocationSize(self: *const VideoFrame, options_: ?CopyToOptions) !u32 {
return parsed.layout.allocation_size;
}
fn copyTo(self: *const VideoFrame, destination: js.AllowSharedBufferSource, options_: ?CopyToOptions, exec: *const Execution) !js.Promise {
fn copyTo(self: *const VideoFrame, destination_value: js.Value, options_: ?CopyToOptions, exec: *const Execution) !js.Promise {
const local = exec.js.local.?;
const destination = local.jsValueToZig(js.AllowSharedBufferSource, destination_value) catch {
return local.rejectPromise(.{ .type_error = "" });
};
const layouts = self.copyInto(destination.bytes, options_ orelse .{}) catch |err| {
return local.rejectPromise(switch (err) {
error.TypeError => .{ .type_error = "" },