Firefox · Toolkit
CVE-2026-109742
Logic Error in Toolkit
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftoolkit/components/pdfjs/content/PdfJsParent.sys.mjs |
modified | |
add_tasktoolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js |
modified | |
add_tasktoolkit/components/pdfjs/test/unit/test_ml_guess_request.js |
modified | |
fortoolkit/components/pdfjs/test/unit/test_ml_guess_request.js |
modified |
Files Changed
toolkit/components/pdfjs/content/PdfJsParent.sys.mjstoolkit/components/pdfjs/test/browser.tomltoolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.jstoolkit/components/pdfjs/test/unit/test_ml_guess_request.jstoolkit/components/pdfjs/test/unit/xpcshell.toml
Patch
diff --git a/toolkit/components/pdfjs/content/PdfJsParent.sys.mjs b/toolkit/components/pdfjs/content/PdfJsParent.sys.mjs
index bf66210642e..940827c2e85 100644
--- a/toolkit/components/pdfjs/content/PdfJsParent.sys.mjs
+++ b/toolkit/components/pdfjs/content/PdfJsParent.sys.mjs
@@ -58,16 +58,18 @@ let gFindTypes = [
"finddiacriticmatchingchange",
];
-// Defence-in-depth bounds for the signature-verification IPC handlers
-// below. Even though only our own viewer code is expected to call them,
-// the IPC boundary is privileged (chrome receives content-supplied
-// data) so we reject obviously malformed shapes before reaching NSS or
-// `about:certificate`. The validators below are exported so xpcshell
-// can exercise them without having to instantiate the JSWindowActor.
+// Defence-in-depth validation for content-supplied signature and alt-text IPC
+// payloads before they reach NSS, `about:certificate`, or the ML engine.
+// Exported for direct xpcshell coverage.
const MAX_CERTS_PER_VIEW = 16;
const MAX_DER_BASE64_LEN = 64 * 1024; // 64 KiB / cert is far above any real chain.
const BASE64_RE = /^[A-Za-z0-9+/=]+$/;
+// The four properties sent by the viewer for an ImageData buffer.
+const ML_GUESS_KEYS = ["data", "width", "height", "channels"];
+// RawImage supports one to four channels.
+const MAX_ML_GUESS_CHANNELS = 4;
+
// `instanceof Uint8Array` is unreliable across realms (xpcshell exercises
// these validators from another global); brand-check a byte view instead.
const isBytes = x => ArrayBuffer.isView(x) && x.BYTES_PER_ELEMENT === 1;
@@ -120,6 +122,44 @@ export function filterCertsForView(data) {
return certs.length ? certs : null;
}
+/**
+ * Validate an `mlGuess` IPC payload before forwarding it to the ML engine.
+ *
+ * The image-to-text pipeline calls `RawImage.fromURL()` when `url` is present,
+ * while the viewer sends only pixel data and geometry. Reject unknown
+ * properties and require the geometry to match `data`.
+ *
+ * @param {*} request Content-supplied object.
+ * @returns {boolean} `true` for a valid
+ * `{data, width, height, channels}` request.
+ * Exported for unit tests.
+ * @internal
+ */
+export function validateMLGuessRequest(request) {
+ if (request === null || typeof request !== "object") {
+ return false;
+ }
+ const keys = Reflect.ownKeys(request);
+ if (
+ keys.length !== ML_GUESS_KEYS.length ||
+ !keys.every(key => ML_GUESS_KEYS.includes(key))
+ ) {
+ return false;
+ }
+ const { data, width, height, channels } = request;
+ return (
+ isBytes(data) &&
+ Number.isInteger(width) &&
+ width > 0 &&
+ Number.isInteger(height) &&
+ height > 0 &&
+ Number.isInteger(channels) &&
+ channels > 0 &&
+ channels <= MAX_ML_GUESS_CHANNELS &&
+ data.length === width * height * channels
+ );
+}
+
export class PdfJsParent extends JSWindowActorParent {
#signatureStorageChangedObserver = null;
@@ -541,8 +581,9 @@ export class PdfJsParent extends JSWindowActorParent {
PdfJsTelemetry.report(data);
}
- async _mlGuess({ data: { service, request } }) {
- if (service !== IMAGE_TO_TEXT_TASK) {
+ async _mlGuess({ data }) {
+ const { service, request } = data ?? {};
+ if (service !== IMAGE_TO_TEXT_TASK || !validateMLGuessRequest(request)) {
return null;
}
try {
diff --git a/toolkit/components/pdfjs/test/browser.toml b/toolkit/components/pdfjs/test/browser.toml
index 8fcf50fd550..083777358cd 100644
--- a/toolkit/components/pdfjs/test/browser.toml
+++ b/toolkit/components/pdfjs/test/browser.toml
@@ -143,6 +143,9 @@ support-files = ["file_pdfjs_js.pdf"]
["browser_pdfjs_minimum_font_size.js"]
+["browser_pdfjs_mlguess_validation.js"]
+support-files = ["file_pdf_download_link.html"]
+
["browser_pdfjs_multipart_response.js"]
support-files = ["pdf_multipart.sjs"]
diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js b/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js
new file mode 100644
index 00000000000..7f021ce91d4
--- /dev/null
+++ b/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RELATIVE_DIR = "toolkit/components/pdfjs/test/";
+const TESTROOT = "https://example.com/browser/" + RELATIVE_DIR;
+const IMAGE_TO_TEXT_TASK = "moz-image-to-text";
+
+// A valid 2x2 RGBA payload.
+function validRequest() {
+ return {
+ data: new Uint8ClampedArray(2 * 2 * 4),
+ width: 2,
+ height: 2,
+ channels: 4,
+ };
+}
+
+// Send directly through the all-frame PdfJs actor, as a compromised content
+// process could.
+function mlGuess(browser, data) {
+ return SpecialPowers.spawn(browser, [data], payload =>
+ content.windowGlobalChild
+ .getActor("PdfJs")
+ .sendQuery("PDFJS:Parent:mlGuess", payload)
+ );
+}
+
+// Only pixel data with matching geometry may reach the ML pipeline.
+add_task(async function test_mlGuess_only_accepts_raw_pixels() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TESTROOT + "file_pdf_download_link.html" },
+ async function (browser) {
+ // Accepted requests return the automation stub response; rejected
+ // requests return null.
+ Assert.deepEqual(
+ await mlGuess(browser, {
+ service: IMAGE_TO_TEXT_TASK,
+ request: validRequest(),
+ }),
+ { output: "In Automation" },
+ "the payload the viewer sends is forwarded to the engine"
+ );
+
+ const withUrl = validRequest();
+ withUrl.url = "file:///etc/passwd";
+ const insteadOfChannels = validRequest();
+ delete insteadOfChannels.channels;
+ insteadOfChannels.url = "file:///etc/passwd";
+
+ for (const [description, data] of [
+ ["no payload at all", undefined],
+ ["a null payload", null],
+ ["no request", { service: IMAGE_TO_TEXT_TASK }],
+ ["a null request", { service: IMAGE_TO_TEXT_TASK, request: null }],
+ [
+ "an unknown service",
+ { service: "moz-echo", request: validRequest() },
+ ],
+ [
+ "a url to fetch",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { url: "file:///etc/passwd" },
+ },
+ ],
+ [
+ "a url next to valid pixels",
+ { service: IMAGE_TO_TEXT_TASK, request: withUrl },
+ ],
+ [
+ "a url in place of channels",
+ { service: IMAGE_TO_TEXT_TASK, request: insteadOfChannels },
+ ],
+ [
+ "a url as the whole payload",
+ { service: IMAGE_TO_TEXT_TASK, request: "file:///etc/passwd" },
+ ],
+ [
+ "dimensions that don't match the data",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { ...validRequest(), width: 4096 },
+ },
+ ],
+ [
+ "data that isn't bytes",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { ...validRequest(), data: "AAAAAAAAAAAAAAAA" },
+ },
+ ],
+ ]) {
+ Assert.equal(
+ await mlGuess(browser, data),
+ null,
+ `mlGuess with ${description} is rejected`
+ );
+ }
+ }
+ );
+});
diff --git a/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js b/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js
new file mode 100644
index 00000000000..31ea40a7bc5
--- /dev/null
+++ b/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { validateMLGuessRequest } = ChromeUtils.importESModule(
+ "resource://pdf.js/PdfJsParent.sys.mjs"
+);
+
+// Build a request with the viewer's four pixel fields.
+function pixels({ width = 2, height = 2, channels = 4, ...rest } = {}) {
+ return {
+ data: new Uint8ClampedArray(width * height * channels),
+ width,
+ height,
+ channels,
+ ...rest,
+ };
+}
+
+const label = v => (typeof v === "string" ? `"${v}"` : String(v));
+
+add_task(function test_rejects_non_objects() {
+ for (const request of [null, undefined, "", "data", 42, true, () => {}]) {
+ Assert.equal(
+ validateMLGuessRequest(request),
+ false,
+ `${label(request)} must be rejected`
+ );
+ }
+ Assert.equal(validateMLGuessRequest({}), false, "empty object");
+ Assert.equal(validateMLGuessRequest([1, 2, 3, 4]), false, "array");
+});
+
+// The image-to-text pipeline passes `request.url` to `RawImage.fromURL()`.
+add_task(function test_rejects_url_requests() {
+ Assert.equal(
+ validateMLGuessRequest({ url: "file:///etc/passwd" }),
+ false,
+ "bare url request"
+ );
+ Assert.equal(
+ validateMLGuessRequest(pixels({ url: "file:///etc/passwd" })),
+ false,
+ "url smuggled next to a well-formed payload"
+ );
+ const swapped = pixels({ url: "file:///etc/passwd" });
+ delete swapped.channels;
+ Assert.equal(
+ validateMLGuessRequest(swapped),
+ false,
+ "url in place of an expected property (right number of keys)"
+ );
+});
+
+add_task(function test_rejects_unknown_properties() {
+ Assert.equal(
+ validateMLGuessRequest(pixels({ options: { streamer: null } })),
+ false,
+ "extra property"
+ );
+ for (const key of ["data", "width", "height", "channels"]) {
+ const request = pixels();
+ delete request[key];
+ Assert.equal(
+ validateMLGuessRequest(request),
+ false,
+ `missing ${key} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_data() {
+ for (const data of [
+ undefined,
+ null,
+ "AAAA",
+ [0, 0, 0, 0],
+ new ArrayBuffer(16),
+ new Uint16Array(16),
+ new Float32Array(16),
+ ]) {
+ Assert.equal(
+ validateMLGuessRequest(pixels({ data })),
+ false,
+ `data ${data?.constructor?.name ?? label(data)} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_dimensions() {
+ for (const value of [
+ 0,
+ -1,
+ -2048,
+ 1.5,
+ NaN,
+ Infinity,
+ "2",
+ null,
+ undefined,
+ ]) {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: value }),
+ false,
+ `width ${label(value)} must be rejected`
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), height: value }),
+ false,
+ `height ${label(value)} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_channels() {
+ for (const channels of [0, -1, 5, 2.5, NaN, "4", null, undefined]) {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), channels }),
+ false,
+ `channels ${label(channels)} must be rejected`
+ );
+ }
+});
+
+// The data length must match the geometry passed to RawImage.
+add_task(function test_rejects_inconsistent_geometry() {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(15) }),
+ false,
+ "data shorter than the geometry"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(17) }),
+ false,
+ "data longer than the geometry"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: 4 }),
+ false,
+ "geometry wider than data"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(0) }),
+ false,
+ "empty data"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: 2 ** 32, height: 2 ** 32 }),
+ false,
+ "huge geometry"
+ );
+});
+
+add_task(function test_accepts_viewer_payload() {
+ Assert.equal(
+ validateMLGuessRequest(pixels()),
+ true,
+ "2x2 RGBA ImageData (Uint8ClampedArray, as canvas produces)"
+ );
+ Assert.equal(
+ validateMLGuessRequest({
+ data: new Uint8Array(4),
+ width: 2,
+ height: 2,
+ channels: 1,
+ }),
+ true,
+ "2x2 grayscale Uint8Array"
+ );
+ Assert.equal(
+ validateMLGuessRequest(pixels({ width: 1, height: 1 })),
+ true,
+ "single pixel"
+ );
+ Assert.equal(
+ validateMLGuessRequest(pixels({ width: 1024, height: 768 })),
+ true,
+ "larger RGBA payload"
+ );
+});
diff --git a/toolkit/components/pdfjs/test/unit/xpcshell.toml b/toolkit/components/pdfjs/test/unit/xpcshell.toml
index a5a6097a82d..a1c64455a90 100644
--- a/toolkit/components/pdfjs/test/unit/xpcshell.toml
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/toolkit/components/pdfjs/test/browser.toml b/toolkit/components/pdfjs/test/browser.toml
index 8fcf50fd550..083777358cd 100644
--- a/toolkit/components/pdfjs/test/browser.toml
+++ b/toolkit/components/pdfjs/test/browser.toml
@@ -143,6 +143,9 @@ support-files = ["file_pdfjs_js.pdf"]
["browser_pdfjs_minimum_font_size.js"]
+["browser_pdfjs_mlguess_validation.js"]
+support-files = ["file_pdf_download_link.html"]
+
["browser_pdfjs_multipart_response.js"]
support-files = ["pdf_multipart.sjs"]
diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js b/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js
new file mode 100644
index 00000000000..7f021ce91d4
--- /dev/null
+++ b/toolkit/components/pdfjs/test/browser_pdfjs_mlguess_validation.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RELATIVE_DIR = "toolkit/components/pdfjs/test/";
+const TESTROOT = "https://example.com/browser/" + RELATIVE_DIR;
+const IMAGE_TO_TEXT_TASK = "moz-image-to-text";
+
+// A valid 2x2 RGBA payload.
+function validRequest() {
+ return {
+ data: new Uint8ClampedArray(2 * 2 * 4),
+ width: 2,
+ height: 2,
+ channels: 4,
+ };
+}
+
+// Send directly through the all-frame PdfJs actor, as a compromised content
+// process could.
+function mlGuess(browser, data) {
+ return SpecialPowers.spawn(browser, [data], payload =>
+ content.windowGlobalChild
+ .getActor("PdfJs")
+ .sendQuery("PDFJS:Parent:mlGuess", payload)
+ );
+}
+
+// Only pixel data with matching geometry may reach the ML pipeline.
+add_task(async function test_mlGuess_only_accepts_raw_pixels() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TESTROOT + "file_pdf_download_link.html" },
+ async function (browser) {
+ // Accepted requests return the automation stub response; rejected
+ // requests return null.
+ Assert.deepEqual(
+ await mlGuess(browser, {
+ service: IMAGE_TO_TEXT_TASK,
+ request: validRequest(),
+ }),
+ { output: "In Automation" },
+ "the payload the viewer sends is forwarded to the engine"
+ );
+
+ const withUrl = validRequest();
+ withUrl.url = "file:///etc/passwd";
+ const insteadOfChannels = validRequest();
+ delete insteadOfChannels.channels;
+ insteadOfChannels.url = "file:///etc/passwd";
+
+ for (const [description, data] of [
+ ["no payload at all", undefined],
+ ["a null payload", null],
+ ["no request", { service: IMAGE_TO_TEXT_TASK }],
+ ["a null request", { service: IMAGE_TO_TEXT_TASK, request: null }],
+ [
+ "an unknown service",
+ { service: "moz-echo", request: validRequest() },
+ ],
+ [
+ "a url to fetch",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { url: "file:///etc/passwd" },
+ },
+ ],
+ [
+ "a url next to valid pixels",
+ { service: IMAGE_TO_TEXT_TASK, request: withUrl },
+ ],
+ [
+ "a url in place of channels",
+ { service: IMAGE_TO_TEXT_TASK, request: insteadOfChannels },
+ ],
+ [
+ "a url as the whole payload",
+ { service: IMAGE_TO_TEXT_TASK, request: "file:///etc/passwd" },
+ ],
+ [
+ "dimensions that don't match the data",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { ...validRequest(), width: 4096 },
+ },
+ ],
+ [
+ "data that isn't bytes",
+ {
+ service: IMAGE_TO_TEXT_TASK,
+ request: { ...validRequest(), data: "AAAAAAAAAAAAAAAA" },
+ },
+ ],
+ ]) {
+ Assert.equal(
+ await mlGuess(browser, data),
+ null,
+ `mlGuess with ${description} is rejected`
+ );
+ }
+ }
+ );
+});
diff --git a/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js b/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js
new file mode 100644
index 00000000000..31ea40a7bc5
--- /dev/null
+++ b/toolkit/components/pdfjs/test/unit/test_ml_guess_request.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { validateMLGuessRequest } = ChromeUtils.importESModule(
+ "resource://pdf.js/PdfJsParent.sys.mjs"
+);
+
+// Build a request with the viewer's four pixel fields.
+function pixels({ width = 2, height = 2, channels = 4, ...rest } = {}) {
+ return {
+ data: new Uint8ClampedArray(width * height * channels),
+ width,
+ height,
+ channels,
+ ...rest,
+ };
+}
+
+const label = v => (typeof v === "string" ? `"${v}"` : String(v));
+
+add_task(function test_rejects_non_objects() {
+ for (const request of [null, undefined, "", "data", 42, true, () => {}]) {
+ Assert.equal(
+ validateMLGuessRequest(request),
+ false,
+ `${label(request)} must be rejected`
+ );
+ }
+ Assert.equal(validateMLGuessRequest({}), false, "empty object");
+ Assert.equal(validateMLGuessRequest([1, 2, 3, 4]), false, "array");
+});
+
+// The image-to-text pipeline passes `request.url` to `RawImage.fromURL()`.
+add_task(function test_rejects_url_requests() {
+ Assert.equal(
+ validateMLGuessRequest({ url: "file:///etc/passwd" }),
+ false,
+ "bare url request"
+ );
+ Assert.equal(
+ validateMLGuessRequest(pixels({ url: "file:///etc/passwd" })),
+ false,
+ "url smuggled next to a well-formed payload"
+ );
+ const swapped = pixels({ url: "file:///etc/passwd" });
+ delete swapped.channels;
+ Assert.equal(
+ validateMLGuessRequest(swapped),
+ false,
+ "url in place of an expected property (right number of keys)"
+ );
+});
+
+add_task(function test_rejects_unknown_properties() {
+ Assert.equal(
+ validateMLGuessRequest(pixels({ options: { streamer: null } })),
+ false,
+ "extra property"
+ );
+ for (const key of ["data", "width", "height", "channels"]) {
+ const request = pixels();
+ delete request[key];
+ Assert.equal(
+ validateMLGuessRequest(request),
+ false,
+ `missing ${key} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_data() {
+ for (const data of [
+ undefined,
+ null,
+ "AAAA",
+ [0, 0, 0, 0],
+ new ArrayBuffer(16),
+ new Uint16Array(16),
+ new Float32Array(16),
+ ]) {
+ Assert.equal(
+ validateMLGuessRequest(pixels({ data })),
+ false,
+ `data ${data?.constructor?.name ?? label(data)} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_dimensions() {
+ for (const value of [
+ 0,
+ -1,
+ -2048,
+ 1.5,
+ NaN,
+ Infinity,
+ "2",
+ null,
+ undefined,
+ ]) {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: value }),
+ false,
+ `width ${label(value)} must be rejected`
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), height: value }),
+ false,
+ `height ${label(value)} must be rejected`
+ );
+ }
+});
+
+add_task(function test_rejects_bad_channels() {
+ for (const channels of [0, -1, 5, 2.5, NaN, "4", null, undefined]) {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), channels }),
+ false,
+ `channels ${label(channels)} must be rejected`
+ );
+ }
+});
+
+// The data length must match the geometry passed to RawImage.
+add_task(function test_rejects_inconsistent_geometry() {
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(15) }),
+ false,
+ "data shorter than the geometry"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(17) }),
+ false,
+ "data longer than the geometry"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: 4 }),
+ false,
+ "geometry wider than data"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), data: new Uint8ClampedArray(0) }),
+ false,
+ "empty data"
+ );
+ Assert.equal(
+ validateMLGuessRequest({ ...pixels(), width: 2 ** 32, height: 2 ** 32 }),
+ false,
+ "huge geometry"
+ );
+});
+
+add_task(function test_accepts_viewer_payload() {
+ Assert.equal(
+ validateMLGuessRequest(pixels()),
+ true,
+ "2x2 RGBA ImageData (Uint8ClampedArray, as canvas produces)"
+ );
+ Assert.equal(
+ validateMLGuessRequest({
+ data: new Uint8Array(4),
+ width: 2,
+ height: 2,
+ channels: 1,
+ }),
+ true,
+ "2x2 grayscale Uint8Array"
+ );
+ Assert.equal(
... (truncated)
Loading diff…
References
On This Page