[wasm] Webcil-in-WebAssembly (#85932)
authorAleksey Kliger (λgeek) <alklig@microsoft.com>
Tue, 16 May 2023 20:10:38 +0000 (16:10 -0400)
committerGitHub <noreply@github.com>
Tue, 16 May 2023 20:10:38 +0000 (16:10 -0400)
commit55c4e8c349da1f0b59d1ff3601aa202b7c97e178
tree77b3e8b39b553713a07d540d90879922c350cb40
parent4c23ac2badc7d839a95ef485149e4fbb53da4695
[wasm] Webcil-in-WebAssembly (#85932)

Define a WebAssembly module wrapper for Webcil assemblies.
Contributes to #80807

### Why

In some settings serving `application/octet-stream` data, or files with weird extensions will trigger firewalls or AV tools.  But let's assume that if you're interested in deploying a .NET WebAssembly app, you're in an environment that can at least serve WebAssembly modules.

### How

Essentially we serve this WebAssembly module:

```wat
(module
  (data "\0f\00\00\00") ;; data segment 0: payload size
  (data "webcil Payload\cc")  ;; data segment 1: webcil payload
  (memory (import "webcil" "memory") 1)
  (global (export "webcilVersion") i32 (i32.const 0))
  (func (export "getWebcilSize") (param $destPtr i32) (result)
    local.get $destPtr
    i32.const 0
    i32.const 4
    memory.init 0)
  (func (export "getWebcilPayload") (param $d i32) (param $n i32) (result)
    local.get $d
    i32.const 0
    local.get $n
    memory.init 1))
```

The module exports two WebAssembly functions `getWebcilSize` and `getWebcilPayload` that write some bytes (being the size or payload of the webcil assembly) to the linear memory at a given offset.  The module also exports the constant `webcilVersion` to version the wrapper format.

So a runtime or tool that wants to consume the webcil module can do something like:

```js
const wasmModule = new WebAssembly.Module (...);
const wasmMemory = new WebAssembly.Memory ({initial: 1});
const wasmInstance =
      new WebAssembly.Instance(wasmModule, {webcil: {memory: wasmMemory}});
const { getWebcilPayload, webcilVersion, getWebcilSize } = wasmInstance.exports;
console.log (`Version ${webcilVersion.value}`);
getWebcilSize(0);
const size = new Int32Array (wasmMemory.buffer)[0]
console.log (`Size ${size}`);
console.log (new Uint8Array(wasmMemory.buffer).subarray(0, 20));
getWebcilPayload(4, size);
console.log (new Uint8Array(wasmMemory.buffer).subarray(0, 20));
```

### How (Part 2)

But actually, we will define the wrapper to consist of exactly 2 data segments in the WebAssembly data section: segment 0 is 4 bytes and encodes the webcil payload size; and segment 1 is of variable size and contains the webcil payload.

So to load a webcil-in-wasm module, the runtime gets the _raw bytes_ of the WebAssembly module (ie: without instantiating it), and parses it to find the data section, assert that there are 2 segments, ensure they're both passive, and get the data directly from segment 1.

---

* Add option to emit webcil inside a wasm module wrapper

* [mono][loader] implement a webcil-in-wasm reader

* reword WebcilWasmWrapper summary comment

* update the Webcil spec to include the WebAssembly wrapper module

* Adjust RVA map offsets to account for wasm prefix

   MonoImage:raw_data is used as a base when applying the RVA map to map virtual addresses to physical offsets in the assembly.  With webcil-in-wasm there's an extra wasm prefix before the webcil payload starts, so we need to account for this extra data when creating the mapping.

   An alternative is to compute the correct offsets as part of generating the webcil, but that would entangle the wasm module and the webcil payload.  The current (somewhat hacky approach) keeps them logically separate.

* Add a note about the rva mapping to the spec

* Serve webcil-in-wasm as .wasm

* remove old .webcil support from Sdk Pack Tasks

* Implement support for webcil in wasm in the managed WebcilReader

* align webcil payload to a 4-byte boundary within the wasm module

   Add padding to data segment 0 to ensure that data segment 1's payload (ie the webcil content itself) is 4-byte aligned

* assert that webcil raw data is 4-byte aligned

* add 4-byte alignment requirement to the webcil spec

* Don't modify MonoImageStorage:raw_data

   instead just keep track of the webcil offset in the MonoImageStorage.

   This introduces a situation where MonoImage:raw_data is different from MonoImageStorage:raw_data.  The one to use for accessing IL and metadata is MonoImage:raw_data.

   The storage pointer is just used by the image loading machinery

---------

Co-authored-by: Larry Ewing <lewing@microsoft.com>
28 files changed:
docs/design/mono/webcil.md
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WasmModuleReader.cs [new file with mode: 0644]
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilConverter.cs
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilReader.cs
src/libraries/Microsoft.NET.WebAssembly.Webcil/src/Webcil/WebcilWasmWrapper.cs [new file with mode: 0644]
src/mono/mono/metadata/assembly.c
src/mono/mono/metadata/image.c
src/mono/mono/metadata/metadata-internals.h
src/mono/mono/metadata/mono-debug.c
src/mono/mono/metadata/webcil-loader.c
src/mono/mono/metadata/webcil-loader.h
src/mono/mono/mini/monovm.c
src/mono/mono/utils/CMakeLists.txt
src/mono/mono/utils/wasm-module-reader.c [new file with mode: 0644]
src/mono/mono/utils/wasm-module-reader.h [new file with mode: 0644]
src/mono/mono/utils/wasm-sections.def [new file with mode: 0644]
src/mono/sample/wasm/browser-advanced/index.html
src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
src/mono/wasm/build/WasmApp.targets
src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs
src/tasks/Common/Utils.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
src/tasks/WasmAppBuilder/WasmAppBuilder.cs