From d250ecedf5be1decfc781144651a7d1edde3fcba Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 24 Jul 2026 20:22:15 -0500 Subject: [PATCH 1/2] [browser][mono] Make sure we treat the 4GB ptrs as unsigned Backport of #121221 (f813007d092219b23d3becc783ec273ecec4f12a) to release/10.0. The wasm/JS boundary does not preserve signedness: a wasm i32 arrives in JS as a signed number, so pointers above 2GB show up negative and silently corrupt data. This coerces those pointers back to unsigned across the mono browser runtime. The upstream commit also touched src/native/libs/System.Native.Browser (CoreCLR-on-wasm), which does not exist in release/10.0; those files are excluded from this backport. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5d0b1c48-73de-4531-aeff-49ed82385c20 --- src/mono/browser/runtime/crypto.ts | 1 + src/mono/browser/runtime/debug.ts | 6 +- .../browser/runtime/diagnostics/common.ts | 2 +- src/mono/browser/runtime/diagnostics/index.ts | 2 +- .../runtime/jiterpreter-interp-entry.ts | 11 ++-- .../browser/runtime/jiterpreter-jit-call.ts | 14 ++++- .../browser/runtime/jiterpreter-support.ts | 63 ++++--------------- .../runtime/jiterpreter-trace-generator.ts | 11 ++-- src/mono/browser/runtime/jiterpreter.ts | 19 +++--- src/mono/browser/runtime/marshal.ts | 5 +- src/mono/browser/runtime/memory.ts | 13 ++-- src/mono/browser/runtime/roots.ts | 4 +- src/mono/browser/runtime/startup.ts | 6 +- src/mono/browser/runtime/strings.ts | 12 ++-- src/mono/browser/runtime/web-socket.ts | 10 +-- 15 files changed, 82 insertions(+), 97 deletions(-) diff --git a/src/mono/browser/runtime/crypto.ts b/src/mono/browser/runtime/crypto.ts index a6642d874ffcbb..a782b3a3874771 100644 --- a/src/mono/browser/runtime/crypto.ts +++ b/src/mono/browser/runtime/crypto.ts @@ -10,6 +10,7 @@ export function mono_wasm_browser_entropy (bufferPtr: number, bufferLength: numb return -1; } + bufferPtr = bufferPtr >>> 0; const memoryView = localHeapViewU8(); const targetView = memoryView.subarray(bufferPtr, bufferPtr + bufferLength); diff --git a/src/mono/browser/runtime/debug.ts b/src/mono/browser/runtime/debug.ts index 522c3c41158b28..45d1714cc84a17 100644 --- a/src/mono/browser/runtime/debug.ts +++ b/src/mono/browser/runtime/debug.ts @@ -6,7 +6,7 @@ import { toBase64StringImpl } from "./base64"; import cwraps from "./cwraps"; import { VoidPtr, CharPtr } from "./types/emscripten"; import { mono_log_warn } from "./logging"; -import { forceThreadMemoryViewRefresh, free, localHeapViewU8, malloc } from "./memory"; +import { forceThreadMemoryViewRefresh, fixupPointer, free, localHeapViewU8, malloc } from "./memory"; import { utf8ToString } from "./strings"; const commands_received: any = new Map(); commands_received.remove = function (key: number): CommandResponse { @@ -42,12 +42,12 @@ export function mono_wasm_fire_debugger_agent_message_with_data_to_pause (base64 } export function mono_wasm_fire_debugger_agent_message_with_data (data: number, len: number): void { - const base64String = toBase64StringImpl(new Uint8Array(localHeapViewU8().buffer, data, len)); + const base64String = toBase64StringImpl(new Uint8Array(localHeapViewU8().buffer, fixupPointer(data, 0), len)); mono_wasm_fire_debugger_agent_message_with_data_to_pause(base64String); } export function mono_wasm_add_dbg_command_received (res_ok: boolean, id: number, buffer: number, buffer_len: number): void { - const dbg_command = new Uint8Array(localHeapViewU8().buffer, buffer, buffer_len); + const dbg_command = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer, 0), buffer_len); const base64String = toBase64StringImpl(dbg_command); const buffer_obj = { res_ok, diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts index dab4823b6b78ac..161480a5435962 100644 --- a/src/mono/browser/runtime/diagnostics/common.ts +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -51,7 +51,7 @@ export class DiagnosticConnectionBase { } const message = this.messagesReceived[0]!; const bytes_read = Math.min(message.length, bytes_to_read); - Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any); + Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any >>> 0); if (bytes_read === message.length) { this.messagesReceived.shift(); } else { diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts index 660ac47d8dcc82..c46fbd7822f077 100644 --- a/src/mono/browser/runtime/diagnostics/index.ts +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -40,7 +40,7 @@ export function setRuntimeGlobals (globalObjects: GlobalObjects): void { if (!wrapper) { return -1; } - const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any, bytes_to_write)).slice(); + const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any >>> 0, bytes_to_write)).slice(); return wrapper.send(message); }; diff --git a/src/mono/browser/runtime/jiterpreter-interp-entry.ts b/src/mono/browser/runtime/jiterpreter-interp-entry.ts index f152a70da25706..176ecec246ac70 100644 --- a/src/mono/browser/runtime/jiterpreter-interp-entry.ts +++ b/src/mono/browser/runtime/jiterpreter-interp-entry.ts @@ -167,7 +167,7 @@ export function mono_jiterp_free_method_data_interp_entry (imethod: number) { // FIXME: move this counter into C and make it thread safe export function mono_interp_record_interp_entry (imethod: number) { // clear the unbox bit - imethod = imethod & ~0x1; + imethod = (imethod & ~0x1) >>> 0; const info = infoTable[imethod]; // This shouldn't happen but it's not worth crashing over @@ -199,6 +199,10 @@ export function mono_interp_jit_wasm_entry_trampoline ( if (argumentCount > maxInlineArgs) return 0; + imethod = imethod >>> 0; + method = method as any >>> 0 as any; + pParamTypes = pParamTypes as any >>> 0 as any; + const info = new TrampolineInfo( imethod, method, argumentCount, pParamTypes, unbox, hasThisReference, hasReturnValue, defaultImplementation @@ -265,10 +269,9 @@ function flush_wasm_entry_trampoline_jit_queue () { // If the function signature contains types that need stackval_from_data, that'll use // some constant slots, so make some extra space - const constantSlots = (4 * jitQueue.length) + 1; let builder = trampBuilder; if (!builder) { - trampBuilder = builder = new WasmBuilder(constantSlots); + trampBuilder = builder = new WasmBuilder(); builder.defineType( "unbox", @@ -303,7 +306,7 @@ function flush_wasm_entry_trampoline_jit_queue () { WasmValtype.void, true ); } else - builder.clear(constantSlots); + builder.clear(); if (builder.options.wasmBytesLimit <= getCounter(JiterpCounter.BytesGenerated)) { return; diff --git a/src/mono/browser/runtime/jiterpreter-jit-call.ts b/src/mono/browser/runtime/jiterpreter-jit-call.ts index 102e402f284e4c..b0516d1e5c8014 100644 --- a/src/mono/browser/runtime/jiterpreter-jit-call.ts +++ b/src/mono/browser/runtime/jiterpreter-jit-call.ts @@ -182,6 +182,11 @@ function getWasmTableEntry (index: number) { export function mono_interp_invoke_wasm_jit_call_trampoline ( thunkIndex: number, ret_sp: number, sp: number, ftndesc: number, thrown: NativePointer ) { + ret_sp = ret_sp as any >>> 0 as any; + sp = sp as any >>> 0 as any; + ftndesc = ftndesc as any >>> 0 as any; + thrown = thrown as any >>> 0 as any; + const thunk = getWasmTableEntry(thunkIndex); try { thunk(ret_sp, sp, ftndesc, thrown); @@ -234,6 +239,11 @@ export function mono_interp_jit_wasm_jit_call_trampoline ( method: MonoMethod, rmethod: VoidPtr, cinfo: VoidPtr, arg_offsets: VoidPtr, catch_exceptions: number ): void { + method = method as any >>> 0 as any; + rmethod = rmethod as any >>> 0 as any; + cinfo = cinfo as any >>> 0 as any; + arg_offsets = arg_offsets as any >>> 0 as any; + // multiple cinfos can share the same target function, so for that scenario we want to // use the same TrampolineInfo for all of them. if that info has already been jitted // we want to immediately store its pointer into the cinfo, otherwise we add it to @@ -296,7 +306,7 @@ export function mono_interp_flush_jitcall_queue (): void { let builder = trampBuilder; if (!builder) { - trampBuilder = builder = new WasmBuilder(0); + trampBuilder = builder = new WasmBuilder(); // Function type for compiled trampolines builder.defineType( "trampoline", @@ -316,7 +326,7 @@ export function mono_interp_flush_jitcall_queue (): void { builder.defineImportedFunction("i", "begin_catch", "begin_catch", true, getRawCwrap("mono_jiterp_begin_catch")); builder.defineImportedFunction("i", "end_catch", "end_catch", true, getRawCwrap("mono_jiterp_end_catch")); } else - builder.clear(0); + builder.clear(); if (builder.options.wasmBytesLimit <= getCounter(JiterpCounter.BytesGenerated)) { cwraps.mono_jiterp_tlqueue_clear(JitQueue.JitCall); diff --git a/src/mono/browser/runtime/jiterpreter-support.ts b/src/mono/browser/runtime/jiterpreter-support.ts index 5bd39ede34563b..8be68c9cfd4484 100644 --- a/src/mono/browser/runtime/jiterpreter-support.ts +++ b/src/mono/browser/runtime/jiterpreter-support.ts @@ -103,10 +103,8 @@ export class WasmBuilder { traceBuf: Array = []; branchTargets = new Set(); options!: JiterpreterOptions; - constantSlots: Array = []; backBranchOffsets: Array = []; callHandlerReturnAddresses: Array = []; - nextConstantSlot = 0; backBranchTraceLevel = 0; containsSimd!: boolean; @@ -115,14 +113,14 @@ export class WasmBuilder { compressImportNames = false; lockImports = false; - constructor (constantSlotCount: number) { + constructor () { this.stack = [new BlobBuilder()]; - this.clear(constantSlotCount); + this.clear(); this.cfg = new Cfg(this); this.defineType("__cpp_exception", { "ptr": WasmValtype.i32 }, WasmValtype.void, true); } - clear (constantSlotCount: number) { + clear () { this.options = getOptions(); if (this.options.maxModuleSize >= blobBuilderCapacity) throw new Error(`blobBuilderCapacity ${blobBuilderCapacity} is not large enough for jiterpreter-max-module-size of ${this.options.maxModuleSize}`); @@ -154,10 +152,6 @@ export class WasmBuilder { this.traceBuf.length = 0; this.branchTargets.clear(); this.activeBlocks = 0; - this.nextConstantSlot = 0; - this.constantSlots.length = this.options.useConstants ? constantSlotCount : 0; - for (let i = 0; i < this.constantSlots.length; i++) - this.constantSlots[i] = 0; this.backBranchOffsets.length = 0; this.callHandlerReturnAddresses.length = 0; @@ -209,7 +203,6 @@ export class WasmBuilder { const exceptionTag = this.getExceptionTag(); const result: any = { - c: this.getConstants(), m: { h: memory }, }; if (exceptionTag) @@ -328,22 +321,10 @@ export class WasmBuilder { } ptr_const (pointer: number | ManagedPointer | NativePointer) { - let idx = this.options.useConstants ? this.constantSlots.indexOf(pointer) : -1; - if ( - this.options.useConstants && - (idx < 0) && (this.nextConstantSlot < this.constantSlots.length) - ) { - idx = this.nextConstantSlot++; - this.constantSlots[idx] = pointer; - } - - if (idx >= 0) { - this.appendU8(WasmOpcode.get_global); - this.appendLeb(idx); - } else { - // mono_log_info(`Warning: no constant slot for ${pointer} (${this.nextConstantSlot} slots used)`); - this.i32_const(pointer); - } + // mono_log_info(`Warning: no constant slot for ${pointer} (${this.nextConstantSlot} slots used)`); + this.appendU8(WasmOpcode.i32_const); + // i32_const is always signed + this.appendLeb((pointer as any) | 0); } ip_const (value: MintOpcodePtr) { @@ -507,7 +488,7 @@ export class WasmBuilder { this.appendULeb( 1 + // memory (enableWasmEh ? 1 : 0) + // c++ exception tag - importsToEmit.length + this.constantSlots.length + + importsToEmit.length + ((includeFunctionTable !== false) ? 1 : 0) ); @@ -521,14 +502,6 @@ export class WasmBuilder { this.appendU8(ifi.typeIndex); } - for (let i = 0; i < this.constantSlots.length; i++) { - this.appendName("c"); - this.appendName(i.toString(shortNameBase)); - this.appendU8(0x03); // global - this.appendU8(WasmValtype.i32); // all constants are pointers right now - this.appendU8(0x00); // constant - } - // import the native heap this.appendName("m"); this.appendName("h"); @@ -909,7 +882,8 @@ export class WasmBuilder { appendMemarg (offset: number, alignPower: number) { this.appendULeb(alignPower); - this.appendULeb(offset); + // u64 + this.appendULeb(offset >>> 0); } /* @@ -919,10 +893,9 @@ export class WasmBuilder { if (typeof (ptr1) === "string") this.local(ptr1); else - this.i32_const(ptr1); + this.ptr_const(ptr1); this.i32_const(offset); - // FIXME: How do we make sure this has correct semantics for pointers over 2gb? this.appendU8(WasmOpcode.i32_add); } @@ -931,13 +904,6 @@ export class WasmBuilder { throw new Error("Jiterpreter block stack not empty"); return this.stack[0].getArrayView(fullCapacity); } - - getConstants () { - const result: { [key: string]: number } = {}; - for (let i = 0; i < this.constantSlots.length; i++) - result[i.toString(shortNameBase)] = this.constantSlots[i]; - return result; - } } export class BlobBuilder { @@ -1617,7 +1583,7 @@ export function append_profiler_event (builder: WasmBuilder, ip: MintOpcodePtr, throw new Error(`Unimplemented profiler event ${opcode}`); } builder.local("frame"); - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport(event_name); } @@ -1633,7 +1599,7 @@ export function append_safepoint (builder: WasmBuilder, ip: MintOpcodePtr) { builder.block(WasmValtype.void, WasmOpcode.if_); builder.local("frame"); // Not ip_const, because we can't pass relative IP to do_safepoint - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport("safepoint"); builder.endBlock(); } @@ -2020,8 +1986,6 @@ export type JiterpreterOptions = { countBailouts: boolean; // Dump the wasm blob for all compiled traces dumpTraces: boolean; - // Use runtime imports for pointer constants - useConstants: boolean; // Enable performing backward branches without exiting traces noExitBackwardBranches: boolean; // Unwrap gsharedvt wrappers when compiling jitcalls if possible @@ -2062,7 +2026,6 @@ const optionNames: { [jsName: string]: string } = { "estimateHeat": "jiterpreter-estimate-heat", "countBailouts": "jiterpreter-count-bailouts", "dumpTraces": "jiterpreter-dump-traces", - "useConstants": "jiterpreter-use-constants", "eliminateNullChecks": "jiterpreter-eliminate-null-checks", "noExitBackwardBranches": "jiterpreter-backward-branches-enabled", "directJitCalls": "jiterpreter-direct-jit-calls", diff --git a/src/mono/browser/runtime/jiterpreter-trace-generator.ts b/src/mono/browser/runtime/jiterpreter-trace-generator.ts index f9938164b5e112..842d89aebcbda7 100644 --- a/src/mono/browser/runtime/jiterpreter-trace-generator.ts +++ b/src/mono/browser/runtime/jiterpreter-trace-generator.ts @@ -1115,7 +1115,7 @@ export function generateWasmBody ( // Stash obj->vtable->klass so we can do a fast has_parent check later if (canDoFastCheck) builder.local("src_ptr", WasmOpcode.tee_local); - builder.i32_const(klass); + builder.ptr_const(klass); builder.appendU8(WasmOpcode.i32_eq); builder.block(WasmValtype.void, WasmOpcode.if_); // if A @@ -1206,7 +1206,7 @@ export function generateWasmBody ( elementClassOffset = getMemberOffset(JiterpMember.ClassElementClass), destOffset = getArgU16(ip, 1), // Get the class's element class, which is what we will actually type-check against - elementClass = getU32_unaligned(klass + elementClassOffset); + elementClass = getU32_unaligned(klass + elementClassOffset) >>> 0; if (!klass || !elementClass) { record_abort(builder.traceIndex, ip, traceName, "null-klass"); @@ -1234,7 +1234,7 @@ export function generateWasmBody ( builder.local("src_ptr", WasmOpcode.tee_local); builder.appendU8(WasmOpcode.i32_load); builder.appendMemarg(elementClassOffset, 0); - builder.i32_const(elementClass); + builder.ptr_const(elementClass); builder.appendU8(WasmOpcode.i32_eq); // Check klass->rank == 0 @@ -1285,7 +1285,7 @@ export function generateWasmBody ( builder.block(); append_ldloca(builder, getArgU16(ip, 1), 4); const vtable = get_imethod_data(frame, getArgU16(ip, 3)); - builder.i32_const(vtable); + builder.ptr_const(vtable); append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load); builder.callImport("newarr"); // If the newarr operation succeeded, continue, otherwise bailout @@ -1973,6 +1973,7 @@ function append_stloc_tail (builder: WasmBuilder, offset: number, opcodeOrPrefix // This looks wrong but I assure you it's correct. builder.appendULeb(simdOpcode); } + offset = offset >>> 0; const alignment = computeMemoryAlignment(offset, opcodeOrPrefix, simdOpcode); builder.appendMemarg(offset, alignment); invalidate_local(offset); @@ -2335,7 +2336,7 @@ function emit_fieldop ( append_ldloc(builder, objectOffset, WasmOpcode.i32_load); append_ldloc(builder, objectOffset, WasmOpcode.i32_load); builder.i32_const(builder.traceIndex); - builder.i32_const(ip); + builder.ptr_const(ip); builder.callImport("notnull"); } } diff --git a/src/mono/browser/runtime/jiterpreter.ts b/src/mono/browser/runtime/jiterpreter.ts index 6ba17a3ab25f30..d427e4eb907d52 100644 --- a/src/mono/browser/runtime/jiterpreter.ts +++ b/src/mono/browser/runtime/jiterpreter.ts @@ -117,7 +117,7 @@ export class TraceInfo { isVerbose: boolean; constructor (ip: MintOpcodePtr, index: number, isVerbose: number) { - this.ip = ip; + this.ip = ip as any >>> 0 as any; this.index = index; this.isVerbose = !!isVerbose; } @@ -731,18 +731,12 @@ function generate_wasm ( traceIndex: number, methodFullName: string | undefined, backwardBranchTable: Uint16Array | null, presetFunctionPointer: number ): number { - // Pre-allocate a decent number of constant slots - this adds fixed size bloat - // to the trace but will make the actual pointer constants in the trace smaller - // If we run out of constant slots it will transparently fall back to i32_const - // For System.Runtime.Tests we only run out of slots ~50 times in 9100 test cases - const constantSlotCount = 8; - let builder = traceBuilder; if (!builder) { - traceBuilder = builder = new WasmBuilder(constantSlotCount); + traceBuilder = builder = new WasmBuilder(); initialize_builder(builder); } else - builder.clear(constantSlotCount); + builder.clear(); mostRecentOptions = builder.options; @@ -1011,6 +1005,10 @@ export function mono_interp_tier_prepare_jiterpreter ( presetFunctionPointer: number ): number { mono_assert(ip, "expected instruction pointer"); + ip = ip as any >>> 0 as any; + frame = frame as any >>> 0 as any; + method = method as any >>> 0 as any; + startOfBody = startOfBody as any >>> 0 as any; if (!mostRecentOptions) mostRecentOptions = getOptions(); @@ -1084,6 +1082,9 @@ export function mono_interp_tier_prepare_jiterpreter ( export function mono_wasm_free_method_data ( method: MonoMethod, imethod: number, traceIndex: number ) { + method = method as any >>> 0 as any; + imethod = imethod >>> 0; + if (runtimeHelpers.emscriptenBuildOptions.enableDevToolsProfiler) { mono_wasm_profiler_free_method(method); } diff --git a/src/mono/browser/runtime/marshal.ts b/src/mono/browser/runtime/marshal.ts index 5d537670c4d9db..d53d3cfa39fa15 100644 --- a/src/mono/browser/runtime/marshal.ts +++ b/src/mono/browser/runtime/marshal.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { js_owned_gc_handle_symbol, teardown_managed_proxy } from "./gc-handles"; import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; -import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8, localHeapViewF64, localHeapViewI32, localHeapViewU8, _zero_region, forceThreadMemoryViewRefresh, setB8, getB8 } from "./memory"; +import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8, localHeapViewF64, localHeapViewI32, localHeapViewU8, _zero_region, forceThreadMemoryViewRefresh, fixupPointer, setB8, getB8 } from "./memory"; import { mono_wasm_new_external_root } from "./roots"; import { GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull, JSMarshalerArguments, JSFunctionSignature, JSMarshalerType, JSMarshalerArgument, MarshalerToJs, MarshalerToCs, WasmRoot, MarshalerType, PThreadPtr, PThreadPtrNull, VoidPtrNull } from "./types/internal"; import { TypedArray, VoidPtr } from "./types/emscripten"; @@ -93,7 +93,7 @@ export function is_receiver_should_free (args: JSMarshalerArguments): boolean { export function get_sync_done_semaphore_ptr (args: JSMarshalerArguments): VoidPtr { if (!WasmEnableThreads) return VoidPtrNull; mono_assert(args, "Null args"); - return getI32(args + JSMarshalerArgumentOffsets.SyncDoneSemaphorePtr) as any; + return getU32(args + JSMarshalerArgumentOffsets.SyncDoneSemaphorePtr) as any; } export function get_caller_native_tid (args: JSMarshalerArguments): PThreadPtr { @@ -467,6 +467,7 @@ export const enum MemoryViewType { abstract class MemoryView implements IMemoryView { protected constructor (public _pointer: VoidPtr, public _length: number, public _viewType: MemoryViewType) { + this._pointer = fixupPointer(_pointer, 0); } abstract dispose(): void; diff --git a/src/mono/browser/runtime/memory.ts b/src/mono/browser/runtime/memory.ts index 3497f05b8b99bc..06d2bbdda0cd7a 100644 --- a/src/mono/browser/runtime/memory.ts +++ b/src/mono/browser/runtime/memory.ts @@ -65,6 +65,7 @@ function assert_int_in_range (value: Number, min: Number, max: Number) { } export function _zero_region (byteOffset: VoidPtr, sizeBytes: number): void { + byteOffset = fixupPointer(byteOffset, 0); localHeapViewU8().fill(0, byteOffset, byteOffset + sizeBytes); } @@ -82,13 +83,13 @@ export function setB8 (offset: MemOffset, value: number | boolean): void { if (typeof (value) === "number") assert_int_in_range(value, 0, 1); receiveWorkerHeapViews(); - Module.HEAPU8[offset] = boolValue ? 1 : 0; + Module.HEAPU8[offset >>> 0] = boolValue ? 1 : 0; } export function setU8 (offset: MemOffset, value: number): void { assert_int_in_range(value, 0, 0xFF); receiveWorkerHeapViews(); - Module.HEAPU8[offset] = value; + Module.HEAPU8[offset >>> 0] = value; } export function setU16 (offset: MemOffset, value: number): void { @@ -122,7 +123,7 @@ export function setU32 (offset: MemOffset, value: NumberOrPointer): void { export function setI8 (offset: MemOffset, value: number): void { assert_int_in_range(value, -0x80, 0x7F); receiveWorkerHeapViews(); - Module.HEAP8[offset] = value; + Module.HEAP8[offset >>> 0] = value; } export function setI16 (offset: MemOffset, value: number): void { @@ -210,12 +211,12 @@ export function getB32 (offset: MemOffset): boolean { export function getB8 (offset: MemOffset): boolean { receiveWorkerHeapViews(); - return !!(Module.HEAPU8[offset]); + return !!(Module.HEAPU8[offset >>> 0]); } export function getU8 (offset: MemOffset): number { receiveWorkerHeapViews(); - return Module.HEAPU8[offset]; + return Module.HEAPU8[offset >>> 0]; } export function getU16 (offset: MemOffset): number { @@ -256,7 +257,7 @@ export function getF64_unaligned (offset: MemOffset): number { export function getI8 (offset: MemOffset): number { receiveWorkerHeapViews(); - return Module.HEAP8[offset]; + return Module.HEAP8[offset >>> 0]; } export function getI16 (offset: MemOffset): number { diff --git a/src/mono/browser/runtime/roots.ts b/src/mono/browser/runtime/roots.ts index 5b81331c29ff25..1e695c7604d8c9 100644 --- a/src/mono/browser/runtime/roots.ts +++ b/src/mono/browser/runtime/roots.ts @@ -171,7 +171,7 @@ export class WasmRootBufferImpl implements WasmRootBuffer { constructor (offset: VoidPtr, capacity: number, ownsAllocation: boolean, name?: string) { const capacityBytes = capacity * 4; - this.__offset = offset; + this.__offset = offset as any >>> 0 as any; this.__offset32 = offset >>> 2; this.__count = capacity; this.length = capacity; @@ -351,7 +351,7 @@ class WasmExternalRoot implements WasmRoot { } _set_address (address: NativePointer | ManagedPointer): void { - this.__external_address = address; + this.__external_address = address as any >>> 0 as any; this.__external_address_32 = address >>> 2; } diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 0bfa9466553abc..e8c7956350f31d 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -28,7 +28,7 @@ import { populateEmscriptenPool, mono_wasm_init_threads } from "./pthreads"; import { currentWorkerThreadEvents, dotnetPthreadCreated, initWorkerThreadEvents, monoThreadInfo } from "./pthreads"; import { mono_wasm_pthread_ptr, update_thread_info } from "./pthreads"; import { jiterpreter_allocate_tables } from "./jiterpreter-support"; -import { localHeapViewU8, malloc, setU32 } from "./memory"; +import { localHeapViewU8, malloc, setU32, fixupPointer } from "./memory"; import { assertNoProxies } from "./gc-handles"; import { runtimeList } from "./exports"; import { nativeAbort, nativeExit } from "./run"; @@ -685,12 +685,12 @@ export function mono_wasm_asm_loaded (assembly_name: CharPtr, assembly_ptr: numb return; const heapU8 = localHeapViewU8(); const assembly_name_str = assembly_name !== CharPtrNull ? utf8ToString(assembly_name).concat(".dll") : ""; - const assembly_data = new Uint8Array(heapU8.buffer, assembly_ptr, assembly_len); + const assembly_data = new Uint8Array(heapU8.buffer, fixupPointer(assembly_ptr, 0), assembly_len); const assembly_b64 = toBase64StringImpl(assembly_data); let pdb_b64; if (pdb_ptr) { - const pdb_data = new Uint8Array(heapU8.buffer, pdb_ptr, pdb_len); + const pdb_data = new Uint8Array(heapU8.buffer, fixupPointer(pdb_ptr, 0), pdb_len); pdb_b64 = toBase64StringImpl(pdb_data); } diff --git a/src/mono/browser/runtime/strings.ts b/src/mono/browser/runtime/strings.ts index 2b52f82b0320b8..9ae9799c166a22 100644 --- a/src/mono/browser/runtime/strings.ts +++ b/src/mono/browser/runtime/strings.ts @@ -7,7 +7,7 @@ import { mono_wasm_new_root, mono_wasm_new_root_buffer } from "./roots"; import { MonoString, MonoStringNull, WasmRoot, WasmRootBuffer } from "./types/internal"; import { Module } from "./globals"; import cwraps from "./cwraps"; -import { isSharedArrayBuffer, localHeapViewU8, getU32_local, setU16_local, localHeapViewU32, getU16_local, localHeapViewU16, _zero_region, malloc, free } from "./memory"; +import { isSharedArrayBuffer, localHeapViewU8, getU32_local, setU16_local, localHeapViewU32, getU16_local, localHeapViewU16, _zero_region, malloc, free, fixupPointer } from "./memory"; import { NativePointer, CharPtr, VoidPtr } from "./types/emscripten"; export const interned_js_string_table = new Map(); @@ -65,10 +65,12 @@ export function utf8ToStringRelaxed (buffer: Uint8Array): string { export function utf8ToString (ptr: CharPtr): string { const heapU8 = localHeapViewU8(); - return utf8BufferToString(heapU8, ptr as any, heapU8.length - (ptr as any)); + const fixedPtr = fixupPointer(ptr, 0); + return utf8BufferToString(heapU8, fixedPtr, heapU8.length - fixedPtr); } export function utf8BufferToString (heapOrArray: Uint8Array, idx: number, maxBytesToRead: number): string { + idx = fixupPointer(idx, 0); const endIdx = idx + maxBytesToRead; let endPtr = idx; while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; @@ -83,6 +85,8 @@ export function utf8BufferToString (heapOrArray: Uint8Array, idx: number, maxByt } export function utf16ToString (startPtr: number, endPtr: number): string { + startPtr = fixupPointer(startPtr, 0); + endPtr = fixupPointer(endPtr, 0); if (_text_decoder_utf16) { const subArray = viewOrCopy(localHeapViewU8(), startPtr as any, endPtr as any); return _text_decoder_utf16.decode(subArray); @@ -277,8 +281,8 @@ export function viewOrCopy (view: Uint8Array, start: CharPtr, end: CharPtr): Uin // this condition should be eliminated by rollup on non-threading builds const needsCopy = isSharedArrayBuffer(view.buffer); return needsCopy - ? view.slice(start, end) - : view.subarray(start, end); + ? view.slice(start >>> 0, end >>> 0) + : view.subarray(start >>> 0, end >>> 0); } // below is minimal legacy support for Blazor diff --git a/src/mono/browser/runtime/web-socket.ts b/src/mono/browser/runtime/web-socket.ts index b26d5fff443826..a43a0557b163ab 100644 --- a/src/mono/browser/runtime/web-socket.ts +++ b/src/mono/browser/runtime/web-socket.ts @@ -6,7 +6,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import { prevent_timer_throttling } from "./scheduling"; import { Queue } from "./queue"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, createPromiseController, loaderHelpers, mono_assert, Module } from "./globals"; -import { setI32, localHeapViewU8, forceThreadMemoryViewRefresh } from "./memory"; +import { setI32, localHeapViewU8, forceThreadMemoryViewRefresh, fixupPointer } from "./memory"; import { VoidPtr } from "./types/emscripten"; import { PromiseController } from "./types/internal"; import { mono_log_warn } from "./logging"; @@ -72,7 +72,7 @@ export function ws_wasm_create (uri: string, sub_protocols: string[] | null, rec ws[wasm_ws_pending_open_promise] = open_promise_control; ws[wasm_ws_pending_send_promises] = []; ws[wasm_ws_pending_close_promises] = []; - ws[wasm_ws_receive_status_ptr] = receive_status_ptr; + ws[wasm_ws_receive_status_ptr] = fixupPointer(receive_status_ptr, 0); ws.binaryType = "arraybuffer"; const local_on_open = () => { try { @@ -185,7 +185,7 @@ export function ws_wasm_send (ws: WebSocketExtension, buffer_ptr: VoidPtr, buffe return resolvedPromise(); } - const buffer_view = new Uint8Array(localHeapViewU8().buffer, buffer_ptr, buffer_length); + const buffer_view = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer_ptr, 0), buffer_length); const whole_buffer = web_socket_send_buffering(ws, buffer_view, message_type, end_of_message); if (!end_of_message || !whole_buffer) { @@ -232,7 +232,7 @@ export function ws_wasm_receive (ws: WebSocketExtension, buffer_ptr: VoidPtr, bu const { promise, promise_control } = createPromiseController(); const receive_promise_control = promise_control as ReceivePromiseControl; - receive_promise_control.buffer_ptr = buffer_ptr; + receive_promise_control.buffer_ptr = fixupPointer(buffer_ptr, 0); receive_promise_control.buffer_length = buffer_length; receive_promise_queue.enqueue(receive_promise_control); @@ -402,7 +402,7 @@ function web_socket_receive_buffering (ws: WebSocketExtension, event_queue: Queu const count = Math.min(buffer_length, event.data.length - event.offset); if (count > 0) { const sourceView = event.data.subarray(event.offset, event.offset + count); - const bufferView = new Uint8Array(localHeapViewU8().buffer, buffer_ptr, buffer_length); + const bufferView = new Uint8Array(localHeapViewU8().buffer, fixupPointer(buffer_ptr, 0), buffer_length); bufferView.set(sourceView, 0); event.offset += count; } From 01f5e911bb76625ce99f817eae5d42642042dba5 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 24 Jul 2026 20:19:53 -0500 Subject: [PATCH 2/2] [browser] Turn the >2GB memory test into an interop conformance test The wasm/JS boundary does not preserve signedness: a wasm i32 arrives in JavaScript as a signed number, so every pointer above 2GB shows up negative. Missing coercions mostly corrupt data silently (a negative subarray start is clamped relative to the end of the heap) rather than throwing, so the previous scenario - which only checked that a string JSImport kept working - could not detect them. Exercise the interop surface with data integrity assertions while all runtime allocations live above the 2GB boundary: strings, byte[]/int[]/double[] in both directions, memory views, JSObject, exceptions, promises, the JSExport direction, crypto and globalization. Also assert that the jiterpreter does not report code generation failures, which is how invalid pointer encoding shows up in the trace compiler. Verified against 10.0.9 and 11.0.0-preview.6 runtime packs with 2.2GB and 3.6GB of heap wasted before startup: 10.0.9 fails the crypto check (the buffer stays zeroed) and logs jiterpreter code generation failures, 11.0 passes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8b51d9f7-816f-483e-b4cc-ec6e2dc45a6a --- src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs | 6 +- .../WasmBasicTestApp/App/MemoryTest.cs | 341 +++++++++++++++++- .../WasmBasicTestApp/App/wwwroot/main.js | 74 +++- 3 files changed, 403 insertions(+), 18 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs b/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs index 50283071f30bde..a8c11617cdfe7c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/MemoryTests.cs @@ -38,10 +38,14 @@ public async Task AllocateLargeHeapThenRepeatedlyInterop() if (BuildTestBase.IsUsingWorkloads) { - await RunForBuildWithDotnetRun(new BrowserRunOptions( + RunResult result = await RunForBuildWithDotnetRun(new BrowserRunOptions( Configuration: config, TestScenario: "AllocateLargeHeapThenInterop" )); + + Assert.Contains(result.TestOutput, line => line.Contains("Great success, MemoryTest finished without errors.")); + // above the 2GB boundary the jiterpreter used to encode negative pointers and emit invalid wasm modules + Assert.DoesNotContain(result.ConsoleOutput, line => line.Contains("code generation failed")); } } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs index 827c0a151c2c0e..0c4fa216fbf28e 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/MemoryTest.cs @@ -2,23 +2,137 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; -using System.Text.Json; -using System.Text; +using System.Collections.Generic; +using System.Globalization; using System.Runtime.InteropServices.JavaScript; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; public partial class MemoryTest { + private static readonly List s_errors = new(); + [JSImport("countChars", "main.js")] internal static partial int CountChars(string testArray); + [JSImport("echoString", "main.js")] + internal static partial string EchoString(string value); + + [JSImport("createBytes", "main.js")] + internal static partial byte[] CreateBytes(int length); + + [JSImport("sumBytes", "main.js")] + internal static partial int SumBytes(byte[] value); + + [JSImport("sumMemoryView", "main.js")] + internal static partial int SumSpan([JSMarshalAs] Span value); + + [JSImport("sumMemoryView", "main.js")] + internal static partial int SumSegment([JSMarshalAs] ArraySegment value); + + [JSImport("echoInt32Array", "main.js")] + internal static partial int[] EchoInt32Array(int[] value); + + [JSImport("echoDoubleArray", "main.js")] + internal static partial double[] EchoDoubleArray(double[] value); + + [JSImport("createObject", "main.js")] + internal static partial JSObject CreateObject(string name, int value); + + [JSImport("throwError", "main.js")] + internal static partial void ThrowError(string message); + + [JSImport("delayedSum", "main.js")] + internal static partial Task DelayedSum(int a, int b); + + [JSImport("callManagedExports", "main.js")] + internal static partial string CallManagedExports(); + + [JSExport] + internal static int SumBytesManaged(byte[] value) + { + int sum = 0; + foreach (byte b in value) + { + sum += b; + } + + return sum; + } + + [JSExport] + internal static byte[] CreateBytesManaged(int length) + { + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) + { + result[i] = unchecked((byte)(i * 3)); + } + + return result; + } + [JSExport] - internal static void Run() + internal static string EchoStringManaged(string value) => value; + + [JSExport] + internal static async Task RunAsync() + { + ReportAllocationAddress(); + AllocateManagedArrays(); + TierUpInterop(); + + Run("string round trip", StringRoundTrip); + Run("large string marshaling", LargeStringMarshaling); + Run("byte[] JS to C#", BytesFromJs); + Run("byte[] C# to JS", BytesToJs); + Run("int[] round trip", Int32ArrayRoundTrip); + Run("double[] round trip", DoubleArrayRoundTrip); + Run("Span memory view", SpanMemoryView); + Run("ArraySegment memory view", ArraySegmentMemoryView); + Run("JSObject properties", JSObjectProperties); + Run("exception propagation", ExceptionPropagation); + Run("crypto RandomNumberGenerator", CryptoRandomBytes); + Run("globalization", Globalization); + Run("JSExport round trips", ManagedExports); + + await RunAsync("Task round trip", TaskRoundTrip); + + if (s_errors.Count != 0) + { + string message = string.Join(Environment.NewLine, s_errors); + TestOutput.WriteLine(message); + throw new Exception(message); + } + + TestOutput.WriteLine("Great success, MemoryTest finished without errors."); + } + + private static void ReportAllocationAddress() + { + byte[] pinned = GC.AllocateArray(1024, pinned: true); + ulong address; + unsafe + { + fixed (byte* p = pinned) + { + address = (ulong)(nuint)p; + } + } + + TestOutput.WriteLine($"Pinned buffer allocated at 0x{address:X}"); + if (address < 0x8000_0000) + { + s_errors.Add($"Allocations are below the 2GB boundary (0x{address:X}), the test is not exercising >2GB pointers."); + } + } + + private static void AllocateManagedArrays() { // Allocate 250MB managed space above 2GB already wasted before startup const int arrayCnt = 10; int[][] arrayHolder = new int[arrayCnt][]; - string errors = ""; TestOutput.WriteLine("Starting managed array allocation"); for (int i = 0; i < arrayCnt; i++) { @@ -28,11 +142,15 @@ internal static void Run() } catch (Exception ex) { - errors += $"Exception {ex} was thrown on i={i}"; + s_errors.Add($"Exception {ex} was thrown on i={i}"); } } + TestOutput.WriteLine("Finished managed array allocation"); + } + private static void TierUpInterop() + { // call a method many times to trigger tier-up optimization string randomString = GenerateRandomString(1000); try @@ -41,22 +159,218 @@ internal static void Run() { int count = CountChars(randomString); if (count != randomString.Length) - errors += $"CountChars returned {count} instead of {randomString.Length} for {i}-th string."; + { + s_errors.Add($"CountChars returned {count} instead of {randomString.Length} for {i}-th string."); + } } } catch (Exception ex) { - errors += $"Exception {ex} was thrown when CountChars was called in a loop"; + s_errors.Add($"Exception {ex} was thrown when CountChars was called in a loop"); + } + } + + private static void Run(string name, Action test) + { + try + { + test(); + TestOutput.WriteLine($"PASS {name}"); + } + catch (Exception ex) + { + s_errors.Add($"FAIL {name}: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static async Task RunAsync(string name, Func test) + { + try + { + await test(); + TestOutput.WriteLine($"PASS {name}"); + } + catch (Exception ex) + { + s_errors.Add($"FAIL {name}: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new Exception(message); + } + } + + private static void StringRoundTrip() + { + foreach (string value in new[] { "", "a", "Příliš žluťoučký kůň", "🔥 emoji \u00e9\u00e8" }) + { + string result = EchoString(value); + Assert(result == value, $"echo mismatch for '{value}', got '{result}'"); + } + } + + private static void LargeStringMarshaling() + { + string value = string.Concat(new string('ě', 500_000), new string('a', 500_000)); + string result = EchoString(value); + Assert(result.Length == value.Length, $"expected {value.Length} chars, got {result.Length}"); + Assert(result == value, "large string content mismatch"); + } + + private static void BytesFromJs() + { + const int length = 64 * 1024; + byte[] bytes = CreateBytes(length); + Assert(bytes.Length == length, $"expected {length} bytes, got {bytes.Length}"); + for (int i = 0; i < length; i++) + { + Assert(bytes[i] == unchecked((byte)i), $"byte[{i}] expected {unchecked((byte)i)}, got {bytes[i]}"); + } + } + + private static void BytesToJs() + { + byte[] bytes = new byte[64 * 1024]; + int expected = 0; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 7)); + expected += bytes[i]; + } + + int actual = SumBytes(bytes); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void Int32ArrayRoundTrip() + { + int[] values = new int[10_000]; + for (int i = 0; i < values.Length; i++) + { + values[i] = i * 3 - 5; + } + + int[] result = EchoInt32Array(values); + Assert(result.Length == values.Length, $"expected {values.Length} items, got {result.Length}"); + for (int i = 0; i < values.Length; i++) + { + Assert(result[i] == values[i], $"int[{i}] expected {values[i]}, got {result[i]}"); + } + } + + private static void DoubleArrayRoundTrip() + { + double[] values = new double[10_000]; + for (int i = 0; i < values.Length; i++) + { + values[i] = i / 3.0; + } + + double[] result = EchoDoubleArray(values); + Assert(result.Length == values.Length, $"expected {values.Length} items, got {result.Length}"); + for (int i = 0; i < values.Length; i++) + { + Assert(result[i] == values[i], $"double[{i}] expected {values[i]}, got {result[i]}"); + } + } + + private static void SpanMemoryView() + { + byte[] bytes = new byte[32 * 1024]; + int expected = 0; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 11)); + expected += bytes[i]; + } + + int actual = SumSpan(bytes.AsSpan()); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void ArraySegmentMemoryView() + { + byte[] bytes = new byte[32 * 1024]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = unchecked((byte)(i * 13)); + } + + var segment = new ArraySegment(bytes, 1024, 4096); + int expected = 0; + for (int i = 0; i < segment.Count; i++) + { + expected += segment[i]; + } + + int actual = SumSegment(segment); + Assert(actual == expected, $"expected sum {expected}, got {actual}"); + } + + private static void JSObjectProperties() + { + using JSObject obj = CreateObject("answer", 42); + Assert(obj.GetPropertyAsString("name") == "answer", "name property mismatch"); + Assert(obj.GetPropertyAsInt32("value") == 42, "value property mismatch"); + obj.SetProperty("value", 43); + Assert(obj.GetPropertyAsInt32("value") == 43, "value property was not updated"); + } + + private static void ExceptionPropagation() + { + try + { + ThrowError("boom"); } - if (!string.IsNullOrEmpty(errors)) + catch (JSException ex) { - TestOutput.WriteLine(errors); - throw new Exception(errors); + Assert(ex.Message.Contains("boom"), $"unexpected message: {ex.Message}"); + return; } - else + + throw new Exception("expected a JSException"); + } + + private static void CryptoRandomBytes() + { + // the buffer is filled by JavaScript, writing to a wrong offset leaves it silently zeroed + byte[] buffer = new byte[4096]; + RandomNumberGenerator.Fill(buffer); + + int zeros = 0; + foreach (byte b in buffer) { - TestOutput.WriteLine("Great success, MemoryTest finished without errors."); + if (b == 0) + { + zeros++; + } } + + Assert(zeros < buffer.Length / 4, $"buffer looks unfilled, {zeros} zero bytes out of {buffer.Length}"); + } + + private static void Globalization() + { + var culture = new CultureInfo("cs-CZ"); + Assert(string.Compare("a", "b", culture, CompareOptions.None) < 0, "unexpected compare result"); + Assert(new DateTime(2026, 1, 2).ToString("MMMM", culture).Length > 0, "empty month name"); + Assert(Encoding.UTF8.GetString(Encoding.UTF8.GetBytes("žluťoučký")) == "žluťoučký", "UTF8 round trip failed"); + } + + private static void ManagedExports() + { + string errors = CallManagedExports(); + Assert(string.IsNullOrEmpty(errors), errors); + } + + private static async Task TaskRoundTrip() + { + int result = await DelayedSum(20, 22); + Assert(result == 42, $"expected 42, got {result}"); } private static Random random = new Random(); @@ -69,6 +383,7 @@ private static string GenerateRandomString(int stringLength) { stringBuilder.Append(chars[random.Next(chars.Length)]); } + return stringBuilder.ToString(); } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index b2de9206dd3842..9f31e38ec693d2 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -20,6 +20,44 @@ function countChars(str) { return length; } +const memoryTestImports = { + countChars, + echoString: (value) => value, + createBytes: (length) => { + const result = new Uint8Array(length); + for (let i = 0; i < length; i++) { + result[i] = i & 0xFF; + } + return result; + }, + sumBytes: (value) => { + let sum = 0; + for (let i = 0; i < value.length; i++) { + sum += value[i]; + } + return sum; + }, + sumMemoryView: (view) => { + const copy = view.slice(); + let sum = 0; + for (let i = 0; i < copy.length; i++) { + sum += copy[i]; + } + return sum; + }, + echoInt32Array: (value) => value, + echoDoubleArray: (value) => value, + createObject: (name, value) => ({ name, value }), + throwError: (message) => { throw new Error(message); }, + delayedSum: async (a, b) => { + await new Promise(resolve => setTimeout(resolve, 10)); + return a + b; + }, + callManagedExports: () => memoryTestCallManagedExports(), +}; + +let memoryTestCallManagedExports = () => "managed exports were not initialized"; + // Prepare base runtime parameters dotnet .withElementOnExit() @@ -270,10 +308,38 @@ try { exit(0); break; case "AllocateLargeHeapThenInterop": - setModuleImports('main.js', { - countChars - }); - exports.MemoryTest.Run(); + setModuleImports('main.js', memoryTestImports); + memoryTestCallManagedExports = () => { + const errors = []; + const check = (condition, message) => { + if (!condition) { + errors.push(message); + } + }; + + const bytes = new Uint8Array(64 * 1024); + let expected = 0; + for (let i = 0; i < bytes.length; i++) { + bytes[i] = (i * 5) & 0xFF; + expected += bytes[i]; + } + check(exports.MemoryTest.SumBytesManaged(bytes) === expected, "SumBytesManaged returned a wrong sum"); + + const made = exports.MemoryTest.CreateBytesManaged(64 * 1024); + check(made.length === 64 * 1024, `CreateBytesManaged returned ${made.length} bytes`); + for (let i = 0; i < made.length; i++) { + if (made[i] !== ((i * 3) & 0xFF)) { + check(false, `CreateBytesManaged[${i}] is ${made[i]}`); + break; + } + } + + const text = 'ě'.repeat(200000) + 'abc'; + check(exports.MemoryTest.EchoStringManaged(text) === text, "EchoStringManaged returned a different string"); + + return errors.join('; '); + }; + await exports.MemoryTest.RunAsync(); exit(0); break; case "HttpNoStreamingTest":