污染CORS与readPixels
1. 机制:谁把 GPU 表面弄脏
<canvas> 默认 origin-clean。下列上传会标脏(以 WHATWG + WebGL / WebGPU 为准):
- 跨源
HTMLImageElement/HTMLVideoElement/SVGImageElement,且这次请求不是 CORS 成功 - 另一张 已经脏了的 canvas /
OffscreenCanvas/ImageBitmap - WebGPU
importExternalTexture/copyExternalImageToTexture吃了上述源
同源 blob、data URL、fetch + CORS 成功再 createImageBitmap 的,保持干净。
脏了之后:
- 继续
draw*/ submit 给用户看,通常没问题 readPixels、canvas.toBlob/toDataURL抛SecurityError- 把这张 canvas 当源再
texImage2D或画进 2D,脏性跟着走
export function assertGlClean(gl: WebGL2RenderingContext): void {
const pixels = new Uint8Array(4);
try {
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
} catch (error) {
if (error instanceof DOMException && error.name === "SecurityError") {
throw new Error("gpu canvas is tainted; readPixels and toBlob are blocked");
}
throw error;
}
}
不要在热路径用 1×1 readPixels 当探针——它本身就是 CPU–GPU 同步。架构上 跟踪源:所有位图 / 视频必须经过 loader,loader 只产出干净对象。
readPixels 读的是 drawing buffer 左下 原点。脏与否和 Y 翻转无关;把干净像素当 PNG 存仍要翻行,见 NDC 与 Y 轴。
2. 适用:先 CORS,再上传
export async function loadCleanBitmap(url: string): Promise<ImageBitmap> {
const response = await fetch(url, { mode: "cors", credentials: "omit" });
if (!response.ok) {
throw new Error(`fetch ${url} -> ${response.status}`);
}
return createImageBitmap(await response.blob());
}
export function texImageClean(
gl: WebGL2RenderingContext,
texture: WebGLTexture,
bitmap: ImageBitmap,
): void {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
}
credentials: "omit" 避免把 cookie 带到图床;否则 ACAO 不能是 *。fetch 失败是 HTTP 状态,不是「onload 了但 canvas 脏了」。
必须走 <img> / <video> 时,crossOrigin = "anonymous" 必须在设 src 之前。设晚了,这次请求按无 CORS 走;元素能显示,texImage2D / importExternalTexture 仍会 taint。产品表现是「预览正常,点导出才 SecurityError」。
3. 不适用
- 用
img.crossOrigin当「一定干净」。CDN 没回 ACAO 时onload仍可能触发。 - 先
drawImage进 2D 再texImage2D(canvas):多一次拷,脏性照传。 - 指望 WebGPU 绕开 taint。
importExternalTexture({ source: video })对跨源视频同一套 origin-clean。 - 用像素 hash 当场景相等金标。GPU、DPR、
premultipliedAlpha都会改字节。
4. 失败形态
| 症状 | 原因 |
|---|---|
预览正常,toBlob / readPixels SecurityError | 跨源无 CORS,或 crossOrigin 设晚 |
texImage2D 直接抛 | 源已脏,或实现选择在上传点拒绝 |
| 2D 离屏也被脏 | drawImage(glCanvas) 时 GL 表面已脏 |
fetch CORS 失败 | 图床 * + 带 cookie,或没有 ACAO |
| 导出偶发成功 | 有时走了同源缓存,有时走了跨源无 CORS |
视频路径见 视频纹理。2D 导出细节见 Canvas 污染。
权威资料
- WHATWG — Security with canvas elements
- Khronos — WebGL 2.0
- W3C — GPUExternalTexture
- MDN — CORS enabled image
核对日期:2026-08-26