[lld][WebAssembly] Add `--no-growable-memory` (#82890)

We recently added `--initial-heap` - an option that allows one to up the
initial memory size without the burden of having to know exactly how
much is needed.

However, in the process of implementing support for this in Emscripten
(https://github.com/emscripten-core/emscripten/pull/21071), we have
realized that `--initial-heap` cannot support the use-case of
non-growable memories by itself, since with it we don't know what to set
`--max-memory` to.

We have thus agreed to move the above work forward by introducing
another option to the linker (see
https://github.com/emscripten-core/emscripten/pull/21071#discussion_r1491755616),
one that would allow users to explicitly specify they want a
non-growable memory.

This change does this by introducing `--no-growable-memory`: an option
that is mutally exclusive with `--max-memory` (for simplicity - we can
also decide that it should override or be overridable by `--max-memory`.
In Emscripten a similar mix of options results in `--no-growable-memory`
taking precedence). The option specifies that the maximum memory size
should be set to the initial memory size, effectively disallowing memory
growth.

Closes #81932.

GitOrigin-RevId: cb4f94db83d9c4373b485493ef079e318f63bf13
diff --git a/docs/WebAssembly.rst b/docs/WebAssembly.rst
index 3f554de..1dd05d6 100644
--- a/docs/WebAssembly.rst
+++ b/docs/WebAssembly.rst
@@ -135,6 +135,10 @@
 
   Maximum size of the linear memory. Default: unlimited.
 
+.. option:: --no-growable-memory
+
+  Set maximum size of the linear memory to its initial size, disallowing memory growth.
+
 By default the function table is neither imported nor exported, but defined
 for internal use only.
 
diff --git a/test/wasm/data-layout.s b/test/wasm/data-layout.s
index 2a447aa..a68bc03 100644
--- a/test/wasm/data-layout.s
+++ b/test/wasm/data-layout.s
@@ -103,6 +103,22 @@
 # CHECK-MAX-NEXT:         Minimum:         0x2
 # CHECK-MAX-NEXT:         Maximum:         0x2
 
+# RUN: wasm-ld --no-entry --initial-memory=327680 --no-growable-memory \
+# RUN:     -o %t_max.wasm %t.hello32.o
+# RUN: obj2yaml %t_max.wasm | FileCheck %s -check-prefix=CHECK-NO-GROWTH
+
+# CHECK-NO-GROWTH:        - Type:            MEMORY
+# CHECK-NO-GROWTH-NEXT:     Memories:
+# CHECK-NO-GROWTH-NEXT:       - Flags:           [ HAS_MAX ]
+# CHECK-NO-GROWTH-NEXT:         Minimum:         0x5
+# CHECK-NO-GROWTH-NEXT:         Maximum:         0x5
+
+# RUN: not wasm-ld --max-memory=262144 --no-growable-memory \
+# RUN:     --no-entry -o %t_max.wasm %t.hello32.o 2>&1 \
+# RUN: | FileCheck %s --check-prefix CHECK-NO-GROWTH-COMPAT-ERROR
+
+# CHECK-NO-GROWTH-COMPAT-ERROR: --max-memory is incompatible with --no-growable-memory
+
 # RUN: wasm-ld -no-gc-sections --allow-undefined --no-entry --shared-memory \
 # RUN:     --features=atomics,bulk-memory --initial-memory=131072 \
 # RUN:     --max-memory=131072 -o %t_max.wasm %t32.o %t.hello32.o
diff --git a/wasm/Config.h b/wasm/Config.h
index 97c508b..266348f 100644
--- a/wasm/Config.h
+++ b/wasm/Config.h
@@ -78,6 +78,7 @@
   uint64_t initialHeap;
   uint64_t initialMemory;
   uint64_t maxMemory;
+  bool noGrowableMemory;
   // The table offset at which to place function addresses.  We reserve zero
   // for the null function pointer.  This gets set to 1 for executables and 0
   // for shared libraries (since they always added to a dynamic offset at
diff --git a/wasm/Driver.cpp b/wasm/Driver.cpp
index 635f19f..df7d4d1 100644
--- a/wasm/Driver.cpp
+++ b/wasm/Driver.cpp
@@ -542,9 +542,15 @@
   config->initialHeap = args::getInteger(args, OPT_initial_heap, 0);
   config->initialMemory = args::getInteger(args, OPT_initial_memory, 0);
   config->maxMemory = args::getInteger(args, OPT_max_memory, 0);
+  config->noGrowableMemory = args.hasArg(OPT_no_growable_memory);
   config->zStackSize =
       args::getZOptionValue(args, OPT_z, "stack-size", WasmPageSize);
 
+  if (config->maxMemory != 0 && config->noGrowableMemory) {
+    // Erroring out here is simpler than defining precedence rules.
+    error("--max-memory is incompatible with --no-growable-memory");
+  }
+
   // Default value of exportDynamic depends on `-shared`
   config->exportDynamic =
       args.hasFlag(OPT_export_dynamic, OPT_no_export_dynamic, config->shared);
diff --git a/wasm/Options.td b/wasm/Options.td
index 8190717..70b5aad 100644
--- a/wasm/Options.td
+++ b/wasm/Options.td
@@ -230,6 +230,9 @@
 def max_memory: JJ<"max-memory=">,
   HelpText<"Maximum size of the linear memory">;
 
+def no_growable_memory: FF<"no-growable-memory">,
+  HelpText<"Set maximum size of the linear memory to its initial size">;
+
 def no_entry: FF<"no-entry">,
   HelpText<"Do not output any entry point">;
 
diff --git a/wasm/Writer.cpp b/wasm/Writer.cpp
index d1a06c9..55eff99 100644
--- a/wasm/Writer.cpp
+++ b/wasm/Writer.cpp
@@ -473,6 +473,7 @@
     WasmSym::heapEnd->setVA(memoryPtr);
   }
 
+  uint64_t maxMemory = 0;
   if (config->maxMemory != 0) {
     if (config->maxMemory != alignTo(config->maxMemory, WasmPageSize))
       error("maximum memory must be " + Twine(WasmPageSize) + "-byte aligned");
@@ -481,20 +482,23 @@
     if (config->maxMemory > maxMemorySetting)
       error("maximum memory too large, cannot be greater than " +
             Twine(maxMemorySetting));
+
+    maxMemory = config->maxMemory;
+  } else if (config->noGrowableMemory) {
+    maxMemory = memoryPtr;
   }
 
-  // Check max if explicitly supplied or required by shared memory
-  if (config->maxMemory != 0 || config->sharedMemory) {
-    uint64_t max = config->maxMemory;
-    if (max == 0) {
-      // If no maxMemory config was supplied but we are building with
-      // shared memory, we need to pick a sensible upper limit.
-      if (ctx.isPic)
-        max = maxMemorySetting;
-      else
-        max = memoryPtr;
-    }
-    out.memorySec->maxMemoryPages = max / WasmPageSize;
+  // If no maxMemory config was supplied but we are building with
+  // shared memory, we need to pick a sensible upper limit.
+  if (config->sharedMemory && maxMemory == 0) {
+    if (ctx.isPic)
+      maxMemory = maxMemorySetting;
+    else
+      maxMemory = memoryPtr;
+  }
+
+  if (maxMemory != 0) {
+    out.memorySec->maxMemoryPages = maxMemory / WasmPageSize;
     log("mem: max pages   = " + Twine(out.memorySec->maxMemoryPages));
   }
 }