Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 720794a

Browse files
committed
Don't proxy primitive instanceof checks by default
This was initially added to make it easier to run Rust workers in Miniflare, as `wasm-bindgen` frequently generates this code. However, the implementation caused `Object` prototype/constructor checks to fail in JavaScript. The previous behaviour can be restored with the `--proxy-primitive`/`miniflare.proxy_primitive_instanceof`/ `proxyPrimitiveInstanceOf` CLI/`wrangler.toml`/API option. Closes #109 Closes #137 Closes #141 Closes cloudflare/workers-sdk#91
1 parent 45a9720 commit 720794a

File tree

15 files changed

+197
-17
lines changed

15 files changed

+197
-17
lines changed

packages/cli-parser/test/help.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Core Options:
5656
--global-timers Allow setting timers outside handlers [boolean]
5757
--global-random Allow secure random generation outside [boolean]
5858
handlers
59+
--proxy-primitive Proxy primitives' instanceof (for WASM) [boolean]
5960
6061
Test Options:
6162
-b, --boolean-option Boolean option [boolean]

packages/core/src/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,12 @@ export class MiniflareCore<
697697
const setupWatch = this.#setupWatch!.get(name);
698698
if (setupWatch) addAll(newWatchPaths, setupWatch);
699699
}
700-
const { modules, processedModuleRules, logUnhandledRejections } =
701-
this.#instances!.CorePlugin;
700+
const {
701+
modules,
702+
processedModuleRules,
703+
logUnhandledRejections,
704+
proxyPrimitiveInstanceOf,
705+
} = this.#instances!.CorePlugin;
702706

703707
// Clean up process-wide promise rejection event listeners
704708
this.#globalScope?.[kDispose]();
@@ -734,7 +738,8 @@ export class MiniflareCore<
734738
globalScope,
735739
script,
736740
rules,
737-
additionalModules
741+
additionalModules,
742+
proxyPrimitiveInstanceOf
738743
);
739744

740745
this.#scriptWatchPaths.clear();

packages/core/src/plugins/core.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export interface CoreOptions {
102102
globalAsyncIO?: boolean;
103103
globalTimers?: boolean;
104104
globalRandom?: boolean;
105+
proxyPrimitiveInstanceOf?: boolean;
105106
}
106107

107108
function mapMountEntries(
@@ -339,6 +340,15 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
339340
})
340341
globalRandom?: boolean;
341342

343+
@Option({
344+
type: OptionType.BOOLEAN,
345+
name: "proxy-primitive",
346+
description: "Proxy primitives' instanceof (for WASM)",
347+
logName: "Proxy Primitives' instanceof",
348+
fromWrangler: ({ miniflare }) => miniflare?.proxy_primitive_instanceof,
349+
})
350+
proxyPrimitiveInstanceOf?: boolean;
351+
342352
readonly processedModuleRules: ProcessedModuleRule[] = [];
343353

344354
readonly upstreamURL?: URL;
@@ -473,6 +483,76 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
473483
// could be used as an escape hatch if behaviour needs to be different
474484
// locally for any reason
475485
MINIFLARE: true,
486+
487+
// Object, Array, Promise, RegExp, Error, EvalError, RangeError,
488+
// ReferenceError, SyntaxError, TypeError, URIError and Function are
489+
// intentionally omitted. There's a trade-off here, ideally we'd like all
490+
// these checks to pass:
491+
//
492+
// ```js
493+
// import vm from "vm";
494+
//
495+
// const ctx1 = vm.createContext({ objectFunction: () => ({}) });
496+
//
497+
// vm.runInContext("({}) instanceof Object", ctx1); // true
498+
// vm.runInContext("({}).constructor === Object", ctx1); // true
499+
//
500+
// vm.runInContext("objectFunction() instanceof Object", ctx1); // false
501+
// vm.runInContext("objectFunction().constructor === Object", ctx1); // false
502+
//
503+
// const ctx2 = vm.createContext({ Object: Object, objectFunction: () => ({}) });
504+
//
505+
// vm.runInContext("({}) instanceof Object", ctx2); // false
506+
// vm.runInContext("({}).constructor === Object", ctx2); // false
507+
//
508+
// vm.runInContext("objectFunction() instanceof Object", ctx2); // true
509+
// vm.runInContext("objectFunction().constructor === Object", ctx2); // true
510+
// ```
511+
//
512+
// wasm-bindgen (a tool used to make compiling Rust to WebAssembly easier),
513+
// often generates code that looks like `value instanceof Object`.
514+
// We'd like this check to succeed for both objects generated outside of
515+
// the worker (e.g. KV), and inside user code. This is what the
516+
// `proxyPrimitiveInstanceOf` option is for. It replaces `Object` with
517+
// a proxy that overrides `Symbol.hasInstance` with a cross-realm check:
518+
//
519+
// ```js
520+
// function isObject(value) {
521+
// return value !== null && typeof value === "object";
522+
// }
523+
//
524+
// const ObjectProxy = new Proxy(Object, {
525+
// get(target, property, receiver) {
526+
// if (property === Symbol.hasInstance) return isObject;
527+
// return Reflect.get(target, property, receiver);
528+
// },
529+
// });
530+
//
531+
// const ctx3 = vm.createContext({
532+
// Object: ObjectProxy,
533+
// objectFunction: () => ({}),
534+
// });
535+
//
536+
// vm.runInContext("({}) instanceof Object", ctx3); // true
537+
// vm.runInContext("({}).constructor === Object", ctx3); // false
538+
//
539+
// vm.runInContext("objectFunction() instanceof Object", ctx3); // true
540+
// vm.runInContext("objectFunction().constructor === Object", ctx3); // false
541+
// ```
542+
//
543+
// The problem with this option (it used to be the default) is that the
544+
// `constructor`/`prototype` checks fail. These are used quite a lot in
545+
// JS, and this was the cause of several issues:
546+
// - https://github.com/cloudflare/miniflare/issues/109
547+
// - https://github.com/cloudflare/miniflare/issues/137
548+
// - https://github.com/cloudflare/miniflare/issues/141
549+
// - https://github.com/cloudflare/wrangler2/issues/91
550+
//
551+
// The new default behaviour (omitting `Object` completely) still has
552+
// the issue `constructor`/`prototype`/`instanceof` checks for `Object`s
553+
// created outside the sandbox (e.g. KV) would fail, but I think that's
554+
// less likely to be an issue, since the return types are always known
555+
// in those cases.
476556
};
477557

478558
// Process module rules if modules mode was enabled

packages/core/test/index.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,61 @@ test("MiniflareCore: #reload: only runs script if module exports needed when scr
677677
await mf.getPlugins(); // Allow script to run
678678
t.true(calledback);
679679
});
680+
test("MiniflareCore: #reload: primitive constructor/prototype/instanceof checks succeed", async (t) => {
681+
const mf = useMiniflareWithHandler(
682+
{ BindingsPlugin },
683+
{ globals: { outsideObject: {} } },
684+
(globals) => {
685+
const tests = {
686+
// Simulating wasm-bindgen
687+
outsideInstanceOf: globals.outsideObject instanceof globals.Object,
688+
insideInstanceOf: {} instanceof globals.Object,
689+
690+
// https://github.com/cloudflare/miniflare/issues/109
691+
// https://github.com/cloudflare/miniflare/issues/141
692+
outsideConstructor:
693+
globals.outsideObject.constructor === globals.Object,
694+
insideConstructor: {}.constructor === globals.Object,
695+
696+
// https://github.com/cloudflare/miniflare/issues/137
697+
newObject: new globals.Object({ a: 1 }),
698+
699+
// https://github.com/cloudflare/wrangler2/issues/91
700+
outsidePrototype:
701+
Object.getPrototypeOf(globals.outsideObject) ===
702+
globals.Object.prototype,
703+
insidePrototype: Object.getPrototypeOf({}) === globals.Object.prototype,
704+
};
705+
return new globals.Response(JSON.stringify(tests), {
706+
headers: { "Content-Type": "application/json" },
707+
});
708+
}
709+
);
710+
711+
let res = await mf.dispatchFetch("http://localhost/");
712+
t.deepEqual(await res.json(), {
713+
outsideInstanceOf: false, // :(
714+
insideInstanceOf: true,
715+
outsideConstructor: false, // :(
716+
insideConstructor: true,
717+
newObject: { a: 1 },
718+
outsidePrototype: false, // :(
719+
insidePrototype: true,
720+
});
721+
722+
// Check with proxyPrimitiveInstanceOf enabled (for Rust development)
723+
await mf.setOptions({ proxyPrimitiveInstanceOf: true });
724+
res = await mf.dispatchFetch("http://localhost/");
725+
t.deepEqual(await res.json(), {
726+
outsideInstanceOf: true,
727+
insideInstanceOf: true,
728+
outsideConstructor: false,
729+
insideConstructor: false,
730+
newObject: {}, // This is super strange
731+
outsidePrototype: true,
732+
insidePrototype: false,
733+
});
734+
});
680735
test("MiniflareCore: #reload: watches files", async (t) => {
681736
const log = new TestLog();
682737
const tmp = await useTmp(t);

packages/core/test/plugins/bindings.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,8 @@ test("BindingsPlugin: propagates error if service throws", async (t) => {
508508
script: `export default {
509509
fetch: () => { throw new Error("oops"); },
510510
}`,
511+
// Required for instanceOf assertion below to work
512+
proxyPrimitiveInstanceOf: true,
511513
},
512514
},
513515
}

packages/core/test/plugins/core.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ test("CorePlugin: parses options from argv", (t) => {
6363
"--global-async-io",
6464
"--global-timers",
6565
"--global-random",
66+
"--proxy-primitive",
6667
]);
6768
t.deepEqual(options, {
6869
scriptPath: "script.js",
@@ -105,6 +106,7 @@ test("CorePlugin: parses options from argv", (t) => {
105106
globalAsyncIO: true,
106107
globalTimers: true,
107108
globalRandom: true,
109+
proxyPrimitiveInstanceOf: true,
108110
});
109111
options = parsePluginArgv(CorePlugin, [
110112
"-c",
@@ -157,6 +159,7 @@ test("CorePlugin: parses options from wrangler config", async (t) => {
157159
global_async_io: true,
158160
global_timers: true,
159161
global_random: true,
162+
proxy_primitive_instanceof: true,
160163
},
161164
},
162165
configDir
@@ -210,6 +213,7 @@ test("CorePlugin: parses options from wrangler config", async (t) => {
210213
globalAsyncIO: true,
211214
globalTimers: true,
212215
globalRandom: true,
216+
proxyPrimitiveInstanceOf: true,
213217
});
214218
// Check build upload dir defaults to dist
215219
options = parsePluginWranglerConfig(
@@ -243,6 +247,7 @@ test("CorePlugin: logs options", (t) => {
243247
globalAsyncIO: true,
244248
globalTimers: true,
245249
globalRandom: true,
250+
proxyPrimitiveInstanceOf: true,
246251
});
247252
t.deepEqual(logs, [
248253
// script is OptionType.NONE so omitted
@@ -263,6 +268,7 @@ test("CorePlugin: logs options", (t) => {
263268
"Allow Global Async I/O: true",
264269
"Allow Global Timers: true",
265270
"Allow Global Secure Random: true",
271+
"Proxy Primitives' instanceof: true",
266272
]);
267273
// Check logs default wrangler config/package paths
268274
logs = logPluginOptions(CorePlugin, {

packages/jest-environment-miniflare/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,18 @@ export default class MiniflareEnvironment implements JestEnvironment {
169169
}
170170
);
171171

172+
const plugins = await mf.getPlugins();
172173
const mfGlobalScope = await mf.getGlobalScope();
173174
mfGlobalScope.global = global;
174175
mfGlobalScope.self = global;
175176
// Make sure Miniflare's global scope is assigned to Jest's global context,
176177
// even if we didn't run a script because we had no Durable Objects
177-
Object.assign(global, makeProxiedGlobals(/* blockCodeGeneration */ false));
178+
if (plugins.CorePlugin.proxyPrimitiveInstanceOf) {
179+
Object.assign(
180+
global,
181+
makeProxiedGlobals(/* blockCodeGeneration */ false)
182+
);
183+
}
178184
Object.assign(global, mfGlobalScope);
179185

180186
// Add a way of getting bindings in modules mode to allow seeding data.

packages/jest-environment-miniflare/test/fixtures/autoload/autoload.spec.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ test("auto-loads package.json and wrangler.toml", async () => {
66
expect(await res.text()).toBe("test");
77
});
88

9+
test("respects proxy_primitive_instanceof option", async () => {
10+
// Should be auto-loaded from wrangler.toml
11+
const { OBJECT } = getMiniflareBindings();
12+
expect(OBJECT instanceof Object).toBe(true);
13+
expect({} instanceof Object).toBe(true);
14+
});
15+
916
test("auto-loads .env", () => {
1017
const { ENV_KEY } = getMiniflareBindings();
1118
expect(ENV_KEY).toBe("value");

packages/jest-environment-miniflare/test/fixtures/autoload/wrangler.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ bindings = [
55

66
[build.upload]
77
format = "modules"
8+
9+
[miniflare]
10+
proxy_primitive_instanceof = true

packages/jest-environment-miniflare/test/fixtures/core.worker.spec.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,7 @@ describe("uses Jest fake timers", () => {
3939
test("can generate secure random numbers", () => {
4040
crypto.getRandomValues(new Uint8Array(8));
4141
});
42+
43+
test("Object constructor checks succeed", () => {
44+
expect({}.constructor === Object).toBe(true);
45+
});

0 commit comments

Comments
 (0)