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:
Tim Huang 2023-05-17 21:43:48 +00:00
parent 95b97a794c
commit 05d8e8c08e
4 changed files with 194 additions and 43 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);

View File

@ -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);