Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js
Original file line number Diff line number Diff line change
Expand Up @@ -29065,6 +29065,129 @@ describe("Canvas2D", function () {
(0,chai__WEBPACK_IMPORTED_MODULE_4__.expect)(ctx.strokeStyle).to.equal("#00ff00");
});

it("rejects a prototype-spoofed object in place of a Path2D", function () {
// instanceof only walks the prototype chain, and a prototype is assignable, so an
// InstanceOf gate accepted any object wearing Path2D.prototype and then unwrapped it.
// Handing fill() a CanvasGradient this way was an access violation, not a wrong answer.
// Object.create(Path2D.prototype) is the same hole with no native wrap behind it at all.
var ctx = createContext();
var spoofedGradient = ctx.createLinearGradient(0, 0, 10, 10);
Object.setPrototypeOf(spoofedGradient, Path2D.prototype);
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(spoofedGradient instanceof Path2D).to.equal(true);

var bare = Object.create(Path2D.prototype);
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(bare instanceof Path2D).to.equal(true);

var realPath = new Path2D();var _loop = function _loop()
{var impostor = _arr[_i];
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(impostor);}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(impostor);}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {realPath.addPath(impostor);}).to.throw();
// The Path2D() argument is a (Path2D or DOMString) union, so a non-Path2D is string
// data rather than an error. It must not be unwrapped on the way there.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D(impostor);}).to.not.throw();
};for (var _i = 0, _arr = [spoofedGradient, bare]; _i < _arr.length; _i++) {_loop();}
});

it("ignores a prototype-spoofed object assigned to fillStyle or strokeStyle", function () {
// Same defect on the gradient side: the assignment gate accepted anything wearing the
// gradient prototype and stored it, and the next fill unwrapped it as a CanvasGradient.
var ctx = createContext();
var gradient = ctx.createLinearGradient(0, 0, 10, 10);
var spoofedPath = new Path2D();
Object.setPrototypeOf(spoofedPath, Object.getPrototypeOf(gradient));

ctx.fillStyle = "#ff0000";
ctx.strokeStyle = "#00ff00";
ctx.fillStyle = spoofedPath;
ctx.strokeStyle = spoofedPath;

// Per spec an unusable assignment leaves the previous value in place.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(ctx.fillStyle).to.equal("#ff0000");
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(ctx.strokeStyle).to.equal("#00ff00");

// The drawing path must stay usable rather than unwrapping the impostor.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fillRect(0, 0, 10, 10);}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.strokeRect(0, 0, 10, 10);}).to.not.throw();
});

it("rejects a non-Path2D argument to fill and stroke", function () {
// Neither call type-checked the argument before handing it to ObjectWrap::Unwrap,
// which does no checking of its own, so an unrelated object was reinterpreted as a
// NativeCanvasPath2D. stroke() was the worse of the two: it had no gate at all, so
// even a string got there.
// The thrown type is deliberately not asserted: a C++ Napi::TypeError surfaces as a
// JS TypeError on some engines and as an InternalError on the QuickJS Node-API port,
// so only the fact that it throws is portable. Every other throw test here does the same.
var ctx = createContext();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke("x");}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke({});}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(5);}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill({});}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(5);}).to.throw();
});

it("still accepts the valid fill and stroke argument forms", function () {
var ctx = createContext();
var path = new Path2D("M0 0 L10 10");
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill();}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke();}).to.not.throw();
// undefined selects the no-argument overload rather than being a bad Path2D.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(undefined);}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(undefined);}).to.not.throw();
// fill() also takes a fill rule string.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill("evenodd");}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(new String("evenodd"));}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {
ctx.fill({ toString: function toString() {return "nonzero";} });
}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path);}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, "nonzero");}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, new String("evenodd"));}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.stroke(path);}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill("invalid");}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(path, "invalid");}).to.throw();
});

it("rejects a non-Path2D argument to Path2D.addPath", function () {
// addPath had neither a type check nor an arity check, so addPath() unwrapped a
// missing argument and addPath("x") unwrapped a string. See above for why the
// thrown type is not asserted.
var path = new Path2D();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath();}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath("x");}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath({});}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {path.addPath(new Path2D("M0 0 L5 5"));}).to.not.throw();
});

it("rejects an invalid native source argument to drawImage", function () {
// Native-looking impostors must throw, not AV via an unchecked Unwrap.
var ctx = createContext();
var realCanvas = new _native.Canvas();
var spoofedCanvas = Object.create(Object.getPrototypeOf(realCanvas));
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(spoofedCanvas instanceof _native.Canvas).to.equal(true);
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage({}, 0, 0);}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage(new Path2D(), 0, 0);}).to.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.drawImage(spoofedCanvas, 0, 0);}).to.throw();
realCanvas.dispose();
});

it("treats a non-Path2D Path2D() argument as path data", function () {
// The constructor routed on IsObject(), so any object was unwrapped as a Path2D.
// Per the (Path2D or DOMString) union a non-Path2D is stringified instead.
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D({});}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D(5);}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {new Path2D();}).to.not.throw();
// A copy of a real Path2D still copies, and an object that stringifies to path
// data is still parsed as such.
var source = new Path2D("M0 0 L10 10");
var ctx = createContext();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {ctx.fill(new Path2D(source));}).to.not.throw();
(0,chai__WEBPACK_IMPORTED_MODULE_3__.expect)(function () {
ctx.fill(new Path2D({ toString: function toString() {return "M0 0 L10 10";} }));
}).to.not.throw();
});

it("accepts a CanvasGradient as fillStyle", function () {
var ctx = createContext();
var gradient = ctx.createLinearGradient(0, 0, 64, 64);
Expand Down
123 changes: 123 additions & 0 deletions Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,129 @@ describe("Canvas2D", function () {
expect(ctx.strokeStyle).to.equal("#00ff00");
});

it("rejects a prototype-spoofed object in place of a Path2D", function () {
// instanceof only walks the prototype chain, and a prototype is assignable, so an
// InstanceOf gate accepted any object wearing Path2D.prototype and then unwrapped it.
// Handing fill() a CanvasGradient this way was an access violation, not a wrong answer.
// Object.create(Path2D.prototype) is the same hole with no native wrap behind it at all.
const ctx = createContext();
const spoofedGradient: any = ctx.createLinearGradient(0, 0, 10, 10);
Object.setPrototypeOf(spoofedGradient, Path2D.prototype);
expect(spoofedGradient instanceof Path2D).to.equal(true);

const bare: any = Object.create(Path2D.prototype);
expect(bare instanceof Path2D).to.equal(true);

const realPath = new Path2D();
for (const impostor of [spoofedGradient, bare]) {
expect(function () { ctx.fill(impostor); }).to.throw();
expect(function () { ctx.stroke(impostor); }).to.throw();
expect(function () { realPath.addPath(impostor); }).to.throw();
// The Path2D() argument is a (Path2D or DOMString) union, so a non-Path2D is string
// data rather than an error. It must not be unwrapped on the way there.
expect(function () { new Path2D(impostor); }).to.not.throw();
}
});

it("ignores a prototype-spoofed object assigned to fillStyle or strokeStyle", function () {
// Same defect on the gradient side: the assignment gate accepted anything wearing the
// gradient prototype and stored it, and the next fill unwrapped it as a CanvasGradient.
const ctx = createContext();
const gradient = ctx.createLinearGradient(0, 0, 10, 10);
const spoofedPath: any = new Path2D();
Object.setPrototypeOf(spoofedPath, Object.getPrototypeOf(gradient));

ctx.fillStyle = "#ff0000";
ctx.strokeStyle = "#00ff00";
ctx.fillStyle = spoofedPath;
ctx.strokeStyle = spoofedPath;

// Per spec an unusable assignment leaves the previous value in place.
expect(ctx.fillStyle).to.equal("#ff0000");
expect(ctx.strokeStyle).to.equal("#00ff00");

// The drawing path must stay usable rather than unwrapping the impostor.
expect(function () { ctx.fillRect(0, 0, 10, 10); }).to.not.throw();
expect(function () { ctx.strokeRect(0, 0, 10, 10); }).to.not.throw();
});

it("rejects a non-Path2D argument to fill and stroke", function () {
// Neither call type-checked the argument before handing it to ObjectWrap::Unwrap,
// which does no checking of its own, so an unrelated object was reinterpreted as a
// NativeCanvasPath2D. stroke() was the worse of the two: it had no gate at all, so
// even a string got there.
// The thrown type is deliberately not asserted: a C++ Napi::TypeError surfaces as a
// JS TypeError on some engines and as an InternalError on the QuickJS Node-API port,
// so only the fact that it throws is portable. Every other throw test here does the same.
const ctx = createContext();
expect(function () { ctx.stroke("x"); }).to.throw();
expect(function () { ctx.stroke({}); }).to.throw();
expect(function () { ctx.stroke(5); }).to.throw();
expect(function () { ctx.fill({}); }).to.throw();
expect(function () { ctx.fill(5); }).to.throw();
});

it("still accepts the valid fill and stroke argument forms", function () {
const ctx = createContext();
const path = new Path2D("M0 0 L10 10");
expect(function () { ctx.fill(); }).to.not.throw();
expect(function () { ctx.stroke(); }).to.not.throw();
// undefined selects the no-argument overload rather than being a bad Path2D.
expect(function () { ctx.fill(undefined); }).to.not.throw();
expect(function () { ctx.stroke(undefined); }).to.not.throw();
// fill() also takes a fill rule string.
expect(function () { ctx.fill("evenodd"); }).to.not.throw();
expect(function () { ctx.fill(new String("evenodd") as any); }).to.not.throw();
expect(function () {
ctx.fill({ toString: function () { return "nonzero"; } } as any);
}).to.not.throw();
expect(function () { ctx.fill(path); }).to.not.throw();
expect(function () { ctx.fill(path, "nonzero"); }).to.not.throw();
expect(function () { ctx.fill(path, new String("evenodd") as any); }).to.not.throw();
expect(function () { ctx.stroke(path); }).to.not.throw();
expect(function () { ctx.fill("invalid"); }).to.throw();
expect(function () { ctx.fill(path, "invalid"); }).to.throw();
});

it("rejects a non-Path2D argument to Path2D.addPath", function () {
// addPath had neither a type check nor an arity check, so addPath() unwrapped a
// missing argument and addPath("x") unwrapped a string. See above for why the
// thrown type is not asserted.
const path = new Path2D();
expect(function () { path.addPath(); }).to.throw();
expect(function () { path.addPath("x"); }).to.throw();
expect(function () { path.addPath({}); }).to.throw();
expect(function () { path.addPath(new Path2D("M0 0 L5 5")); }).to.not.throw();
});

it("rejects an invalid native source argument to drawImage", function () {
// Native-looking impostors must throw, not AV via an unchecked Unwrap.
const ctx = createContext();
const realCanvas = new _native.Canvas();
const spoofedCanvas = Object.create(Object.getPrototypeOf(realCanvas));
expect(spoofedCanvas instanceof _native.Canvas).to.equal(true);
expect(function () { ctx.drawImage({}, 0, 0); }).to.throw();
expect(function () { ctx.drawImage(new Path2D(), 0, 0); }).to.throw();
expect(function () { ctx.drawImage(spoofedCanvas, 0, 0); }).to.throw();
realCanvas.dispose();
});

it("treats a non-Path2D Path2D() argument as path data", function () {
// The constructor routed on IsObject(), so any object was unwrapped as a Path2D.
// Per the (Path2D or DOMString) union a non-Path2D is stringified instead.
expect(function () { new Path2D({}); }).to.not.throw();
expect(function () { new Path2D(5); }).to.not.throw();
expect(function () { new Path2D(); }).to.not.throw();
// A copy of a real Path2D still copies, and an object that stringifies to path
// data is still parsed as such.
const source = new Path2D("M0 0 L10 10");
const ctx = createContext();
expect(function () { ctx.fill(new Path2D(source)); }).to.not.throw();
expect(function () {
ctx.fill(new Path2D({ toString: function () { return "M0 0 L10 10"; } }));
}).to.not.throw();
});

it("accepts a CanvasGradient as fillStyle", function () {
const ctx = createContext();
const gradient = ctx.createLinearGradient(0, 0, 64, 64);
Expand Down
1 change: 1 addition & 0 deletions Polyfills/Canvas/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ set(SOURCES
"Source/Context.h"
"Source/MeasureText.cpp"
"Source/MeasureText.h"
"Source/NativeInstanceRegistry.h"
"Source/Gradient.cpp"
"Source/Gradient.h"
"Source/Font.cpp"
Expand Down
9 changes: 9 additions & 0 deletions Polyfills/Canvas/Source/Canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "Colors.h"
#include "Gradient.h"
#include "Font.h"
#include "NativeInstanceRegistry.h"
#include <basen.hpp>
#include <bimg/encode.h>
#include <bx/allocator.h>
Expand Down Expand Up @@ -47,15 +48,23 @@ namespace Babylon::Polyfills::Internal
JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func);
}

NativeCanvas* NativeCanvas::TryUnwrap(Napi::Env env, const Napi::Value& value)
{
return NativeInstanceRegistry<NativeCanvas>::TryUnwrap(env, value);
}

NativeCanvas::NativeCanvas(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<NativeCanvas>{info}
, m_graphicsContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())}
, Polyfills::Canvas::Impl::MonitoredResource{Polyfills::Canvas::Impl::GetFromJavaScript(info.Env())}
{
NativeInstanceRegistry<NativeCanvas>::Add(info, this);
}

NativeCanvas::~NativeCanvas()
{
NativeInstanceRegistry<NativeCanvas>::Remove(this);

// Canvas and Context form a JS cycle; finalizer order is not guaranteed.
// Clear the reverse pointer first so Context::~Context cannot touch us.
if (m_context != nullptr)
Expand Down
2 changes: 2 additions & 0 deletions Polyfills/Canvas/Source/Canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ namespace Babylon::Polyfills::Internal
public:
static void Initialize(Napi::Env env);

static NativeCanvas* TryUnwrap(Napi::Env env, const Napi::Value& value);

explicit NativeCanvas(const Napi::CallbackInfo& info);
virtual ~NativeCanvas();

Expand Down
Loading
Loading