mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-03 18:47:53 +00:00
Bug 1833213 - Clone the image data if canvas randomization is enabled in CanvasRenderingContext2D::GetImageDataArray(). r=tjr,lsalzman
When introducing the random noises to the image data in CanvasRenderingContext2D::GetImageDataArray would also alter the actual image buffer. This will change the random noises every time we call the function because the random noises is generated according to the image data. This is undesirable and the random noises should remain the same in the browsing session with the same canvas. To fix this issue, we clone the actual image data if canvas randomization is enabled and introduce noises to the cloned data, which doesn't change the actual image buffer. So, the random nosies will remain consistent in a single browser session. We also fix some minor issues in this patch. Differential Revision: https://phabricator.services.mozilla.com/D178327
This commit is contained in:
parent
95b97a794c
commit
05d8e8c08e
@ -1872,10 +1872,11 @@ UniquePtr<uint8_t[]> CanvasRenderingContext2D::GetImageBuffer(
|
||||
|
||||
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
||||
|
||||
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
nsRFPService::RandomizePixels(GetCookieJarSettings(), ret.get(),
|
||||
GetWidth() * GetHeight() * 4,
|
||||
SurfaceFormat::A8R8G8B8_UINT32);
|
||||
if (ret && ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
nsRFPService::RandomizePixels(
|
||||
GetCookieJarSettings(), ret.get(),
|
||||
out_imageSize->width * out_imageSize->height * 4,
|
||||
SurfaceFormat::A8R8G8B8_UINT32);
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -5829,11 +5830,6 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
|
||||
RefPtr<DataSourceSurface> readback = snapshot->GetDataSurface();
|
||||
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
||||
|
||||
DataSourceSurface::MappedSurface rawData;
|
||||
if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// Check for site-specific permission. This check is not needed if the
|
||||
// canvas was created with a docshell (that is only done for special
|
||||
// internal uses).
|
||||
@ -5847,6 +5843,24 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
|
||||
RFPTarget::CanvasImageExtractionPrompt);
|
||||
}
|
||||
|
||||
// Clone the data source surface if canvas randomization is enabled. We need
|
||||
// to do this because we don't want to alter the actual image buffer.
|
||||
// Otherwise, we will provide inconsistent image data with multiple calls.
|
||||
//
|
||||
// Note that we don't need to clone if we will use the place holder because
|
||||
// the place holder doesn't use actual image data.
|
||||
bool needRandomizePixels = false;
|
||||
if (!usePlaceholder &&
|
||||
ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
needRandomizePixels = true;
|
||||
readback = CreateDataSourceSurfaceByCloning(readback);
|
||||
}
|
||||
|
||||
DataSourceSurface::MappedSurface rawData;
|
||||
if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
do {
|
||||
uint8_t* randomData;
|
||||
if (usePlaceholder) {
|
||||
@ -5866,17 +5880,18 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply the random noises if canvan randomization is enabled.
|
||||
if (needRandomizePixels) {
|
||||
const IntSize size = readback->GetSize();
|
||||
nsRFPService::RandomizePixels(GetCookieJarSettings(), rawData.mData,
|
||||
size.height * size.width * 4,
|
||||
SurfaceFormat::A8R8G8B8_UINT32);
|
||||
}
|
||||
|
||||
uint32_t srcStride = rawData.mStride;
|
||||
uint8_t* src =
|
||||
rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
|
||||
|
||||
// Apply the random noises if canvan randomization is enabled.
|
||||
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
nsRFPService::RandomizePixels(GetCookieJarSettings(), src,
|
||||
GetWidth() * GetHeight() * 4,
|
||||
SurfaceFormat::A8R8G8B8_UINT32);
|
||||
}
|
||||
|
||||
uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
|
||||
|
||||
if (mOpaque) {
|
||||
|
@ -171,10 +171,11 @@ mozilla::UniquePtr<uint8_t[]> ImageBitmapRenderingContext::GetImageBuffer(
|
||||
|
||||
UniquePtr<uint8_t[]> ret = gfx::SurfaceToPackedBGRA(data);
|
||||
|
||||
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
nsRFPService::RandomizePixels(GetCookieJarSettings(), ret.get(),
|
||||
GetWidth() * GetHeight() * 4,
|
||||
gfx::SurfaceFormat::A8R8G8B8_UINT32);
|
||||
if (ret && ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
|
||||
nsRFPService::RandomizePixels(
|
||||
GetCookieJarSettings(), ret.get(),
|
||||
data->GetSize().width * data->GetSize().height * 4,
|
||||
gfx::SurfaceFormat::A8R8G8B8_UINT32);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ var TEST_CASES = [
|
||||
|
||||
const imageData = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return imageData.data;
|
||||
// Access the data again.
|
||||
const imageDataSecond = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return [imageData.data, imageDataSecond.data];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2, isCompareOriginal) {
|
||||
@ -75,7 +78,12 @@ var TEST_CASES = [
|
||||
// Add the canvas element to the document
|
||||
content.document.body.appendChild(canvas);
|
||||
|
||||
return canvas.toDataURL();
|
||||
const dataURL = canvas.toDataURL();
|
||||
|
||||
// Access the data again.
|
||||
const dataURLSecond = canvas.toDataURL();
|
||||
|
||||
return [dataURL, dataURLSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -105,7 +113,12 @@ var TEST_CASES = [
|
||||
// Add the canvas element to the document
|
||||
content.document.body.appendChild(canvas);
|
||||
|
||||
return canvas.toDataURL();
|
||||
const dataURL = canvas.toDataURL();
|
||||
|
||||
// Access the data again.
|
||||
const dataURLSecond = canvas.toDataURL();
|
||||
|
||||
return [dataURL, dataURLSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -138,7 +151,12 @@ var TEST_CASES = [
|
||||
const bitmapContext = bitmapCanvas.getContext("bitmaprenderer");
|
||||
bitmapContext.transferFromImageBitmap(bitmap);
|
||||
|
||||
return bitmapCanvas.toDataURL();
|
||||
const dataURL = bitmapCanvas.toDataURL();
|
||||
|
||||
// Access the data again.
|
||||
const dataURLSecond = bitmapCanvas.toDataURL();
|
||||
|
||||
return [dataURL, dataURLSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -172,7 +190,18 @@ var TEST_CASES = [
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
// Access the data again.
|
||||
let dataSecond = await new content.Promise(resolve => {
|
||||
canvas.toBlob(blob => {
|
||||
let fileReader = new content.FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -212,7 +241,12 @@ var TEST_CASES = [
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
// We don't get the consistent blob data on second access with webgl
|
||||
// context regardless of the canvas randomization. So, we report the
|
||||
// same data here to not fail the test. Ideally, we should look into
|
||||
// why this happens, but it's not caused by canvas randomization.
|
||||
|
||||
return [data, data];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -255,7 +289,18 @@ var TEST_CASES = [
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
// Access the data again.
|
||||
let dataSecond = await new content.Promise(resolve => {
|
||||
bitmapCanvas.toBlob(blob => {
|
||||
let fileReader = new content.FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -284,7 +329,18 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
// Access the data again.
|
||||
let blobSecond = await offscreenCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new content.Promise(resolve => {
|
||||
let fileReader = new content.FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -321,7 +377,18 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
// Access the data again.
|
||||
let blobSecond = await offscreenCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new content.Promise(resolve => {
|
||||
let fileReader = new content.FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -356,7 +423,18 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
// Access the data again.
|
||||
let blobSecond = await bitmapCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new content.Promise(resolve => {
|
||||
let fileReader = new content.FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
@ -377,7 +455,10 @@ var TEST_CASES = [
|
||||
|
||||
const imageData = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return imageData.data;
|
||||
// Access the data again.
|
||||
const imageDataSecond = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return [imageData.data, imageDataSecond.data];
|
||||
});
|
||||
},
|
||||
isDataRandomized(data1, data2, isCompareOriginal) {
|
||||
@ -431,7 +512,7 @@ async function runTest(enabled) {
|
||||
for (let test of TEST_CASES) {
|
||||
info(`Testing ${test.name}`);
|
||||
let data = await test.extractCanvasData(tab.linkedBrowser);
|
||||
let result = test.isDataRandomized(data, test.originalData);
|
||||
let result = test.isDataRandomized(data[0], test.originalData);
|
||||
|
||||
is(
|
||||
result,
|
||||
@ -439,19 +520,29 @@ async function runTest(enabled) {
|
||||
`The image data is ${enabled ? "randomized" : "the same"}.`
|
||||
);
|
||||
|
||||
ok(
|
||||
!test.isDataRandomized(data[0], data[1]),
|
||||
"The data of first and second access should be the same."
|
||||
);
|
||||
|
||||
let privateData = await test.extractCanvasData(privateTab.linkedBrowser);
|
||||
|
||||
// Check if we add noise to canvas data in private windows.
|
||||
result = test.isDataRandomized(privateData, test.originalData, true);
|
||||
result = test.isDataRandomized(privateData[0], test.originalData, true);
|
||||
is(
|
||||
result,
|
||||
enabled,
|
||||
`The private image data is ${enabled ? "randomized" : "the same"}.`
|
||||
);
|
||||
|
||||
ok(
|
||||
!test.isDataRandomized(privateData[0], privateData[1]),
|
||||
"The data of first and second access should be the same for private windows."
|
||||
);
|
||||
|
||||
// Make sure the noises are different between normal window and private
|
||||
// windows.
|
||||
result = test.isDataRandomized(privateData, data);
|
||||
result = test.isDataRandomized(privateData[0], data[0]);
|
||||
is(
|
||||
result,
|
||||
enabled,
|
||||
@ -482,7 +573,8 @@ add_setup(async function() {
|
||||
|
||||
// Extract the original canvas data without random noise.
|
||||
for (let test of TEST_CASES) {
|
||||
test.originalData = await test.extractCanvasData(tab.linkedBrowser);
|
||||
let data = await test.extractCanvasData(tab.linkedBrowser);
|
||||
test.originalData = data[0];
|
||||
}
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
|
@ -57,7 +57,9 @@ var TEST_CASES = [
|
||||
|
||||
const imageData = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return imageData.data;
|
||||
const imageDataSecond = context.getImageData(0, 0, 100, 100);
|
||||
|
||||
return [imageData.data, imageDataSecond.data];
|
||||
},
|
||||
isDataRandomized(data1, data2, isCompareOriginal) {
|
||||
let diffCnt = compareUint8Arrays(data1, data2);
|
||||
@ -100,7 +102,17 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
let blobSecond = await offscreenCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new Promise(resolve => {
|
||||
let fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
return compareArrayBuffer(data1, data2);
|
||||
@ -135,7 +147,17 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
let blobSecond = await offscreenCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new Promise(resolve => {
|
||||
let fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
return compareArrayBuffer(data1, data2);
|
||||
@ -168,7 +190,17 @@ var TEST_CASES = [
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
return data;
|
||||
let blobSecond = await bitmapCanvas.convertToBlob();
|
||||
|
||||
let dataSecond = await new Promise(resolve => {
|
||||
let fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(blobSecond);
|
||||
});
|
||||
|
||||
return [data, dataSecond];
|
||||
},
|
||||
isDataRandomized(data1, data2) {
|
||||
return compareArrayBuffer(data1, data2);
|
||||
@ -209,29 +241,39 @@ async function runTest(enabled) {
|
||||
test.extractCanvasData
|
||||
);
|
||||
|
||||
let result = test.isDataRandomized(data, test.originalData);
|
||||
let result = test.isDataRandomized(data[0], test.originalData);
|
||||
is(
|
||||
result,
|
||||
enabled,
|
||||
`The image data is ${enabled ? "randomized" : "the same"}.`
|
||||
);
|
||||
|
||||
ok(
|
||||
!test.isDataRandomized(data[0], data[1]),
|
||||
"The data of first and second access should be the same."
|
||||
);
|
||||
|
||||
let privateData = await await runFunctionInWorker(
|
||||
privateTab.linkedBrowser,
|
||||
test.extractCanvasData
|
||||
);
|
||||
|
||||
// Check if we add noise to canvas data in private windows.
|
||||
result = test.isDataRandomized(privateData, test.originalData, true);
|
||||
result = test.isDataRandomized(privateData[0], test.originalData, true);
|
||||
is(
|
||||
result,
|
||||
enabled,
|
||||
`The private image data is ${enabled ? "randomized" : "the same"}.`
|
||||
);
|
||||
|
||||
ok(
|
||||
!test.isDataRandomized(privateData[0], privateData[1]),
|
||||
"The data of first and second access should be the same."
|
||||
);
|
||||
|
||||
// Make sure the noises are different between normal window and private
|
||||
// windows.
|
||||
result = test.isDataRandomized(privateData, data);
|
||||
result = test.isDataRandomized(privateData[0], data[0]);
|
||||
is(
|
||||
result,
|
||||
enabled,
|
||||
@ -262,10 +304,11 @@ add_setup(async function() {
|
||||
|
||||
// Extract the original canvas data without random noise.
|
||||
for (let test of TEST_CASES) {
|
||||
test.originalData = await runFunctionInWorker(
|
||||
let data = await runFunctionInWorker(
|
||||
tab.linkedBrowser,
|
||||
test.extractCanvasData
|
||||
);
|
||||
test.originalData = data[0];
|
||||
}
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
|
Loading…
Reference in New Issue
Block a user