diff --git a/emcc.py b/emcc.py
index 5ae47aaab53ec..4eb50e9988563 100755
--- a/emcc.py
+++ b/emcc.py
@@ -2087,6 +2087,7 @@ def phase_linker_setup(options, state, newargs):
     if settings.MAIN_MODULE == 1:
       settings.INCLUDE_FULL_LIBRARY = 1
     settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$loadDylibs']
+    settings.REQUIRED_EXPORTS += ['malloc']
 
   if settings.MAIN_MODULE == 1 or settings.SIDE_MODULE == 1:
     settings.LINKABLE = 1
@@ -2339,7 +2340,7 @@ def phase_linker_setup(options, state, newargs):
       settings.FETCH_WORKER_FILE = unsuffixed_basename(target) + '.fetch.js'
 
   if settings.DEMANGLE_SUPPORT:
-    settings.REQUIRED_EXPORTS += ['__cxa_demangle']
+    settings.REQUIRED_EXPORTS += ['__cxa_demangle', 'free']
     settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$demangle', '$stackTrace']
 
   if settings.FULL_ES3:
@@ -2413,11 +2414,6 @@ def phase_linker_setup(options, state, newargs):
       exit_with_error('-sPROXY_TO_PTHREAD requires -pthread to work!')
     settings.JS_LIBRARIES.append((0, 'library_pthread_stub.js'))
 
-  # TODO: Move this into the library JS file once it becomes possible.
-  # See https://github.com/emscripten-core/emscripten/pull/15982
-  if settings.INCLUDE_FULL_LIBRARY and not settings.DISABLE_EXCEPTION_CATCHING:
-    settings.EXPORTED_FUNCTIONS += ['___get_exception_message', '_free']
-
   if settings.MEMORY64:
     if settings.ASYNCIFY and settings.MEMORY64 == 1:
       exit_with_error('MEMORY64=1 is not compatible with ASYNCIFY')
@@ -2760,20 +2756,16 @@ def check_memory_setting(setting):
     # need to be able to call these explicitly.
     settings.REQUIRED_EXPORTS += ['__funcs_on_exit']
 
-  # various settings require malloc/free support from JS
-  if settings.RELOCATABLE or \
-     settings.BUILD_AS_WORKER or \
-     settings.USE_WEBGPU or \
-     settings.OFFSCREENCANVAS_SUPPORT or \
-     settings.LEGACY_GL_EMULATION or \
+  # Some settings require malloc/free to be exported explictly.
+  # In most cases, the inclustion of native symbols like malloc and free
+  # is taken care of by wasm-ld use its normal symbol resolution process.
+  # However, when JS symbols are exported explictly via
+  # DEFAULT_LIBRARY_FUNCS_TO_INCLUDE and they depend on native symbols
+  # we need to explictly require those exports.
+  if settings.BUILD_AS_WORKER or \
      settings.ASYNCIFY or \
      settings.WASMFS or \
-     settings.DEMANGLE_SUPPORT or \
      settings.FORCE_FILESYSTEM or \
-     settings.STB_IMAGE or \
-     settings.EMBIND or \
-     settings.FETCH or \
-     settings.PROXY_POSIX_SOCKETS or \
      options.memory_profiler or \
      sanitize:
     settings.REQUIRED_EXPORTS += ['malloc', 'free']
diff --git a/src/embind/embind.js b/src/embind/embind.js
index f4c7ca2fa0563..dd56b2e4c02db 100644
--- a/src/embind/embind.js
+++ b/src/embind/embind.js
@@ -371,7 +371,7 @@ var LibraryEmbind = {
     return ret;
   },
 
-  $getTypeName__deps: ['$readLatin1String', '__getTypeName'],
+  $getTypeName__deps: ['$readLatin1String', '__getTypeName', 'free'],
   $getTypeName: function(type) {
     var ptr = ___getTypeName(type);
     var rv = readLatin1String(ptr);
@@ -651,7 +651,7 @@ var LibraryEmbind = {
   _embind_register_std_string__deps: [
     '$readLatin1String', '$registerType',
     '$simpleReadValueFromPointer', '$throwBindingError',
-    '$stringToUTF8', '$lengthBytesUTF8'],
+    '$stringToUTF8', '$lengthBytesUTF8', 'malloc', 'free'],
   _embind_register_std_string: function(rawType, name) {
     name = readLatin1String(name);
     var stdStringIsUTF8
diff --git a/src/library_dylink.js b/src/library_dylink.js
index 471f73cf84ab4..f3ae85930657f 100644
--- a/src/library_dylink.js
+++ b/src/library_dylink.js
@@ -379,7 +379,7 @@ var LibraryDylink = {
   // Allocate memory even if malloc isn't ready yet.  The allocated memory here
   // must be zero initialized since its used for all static data, including bss.
   $getMemory__noleakcheck: true,
-  $getMemory__deps: ['$GOT', '__heap_base', '$zeroMemory'],
+  $getMemory__deps: ['$GOT', '__heap_base', '$zeroMemory', 'malloc'],
   $getMemory: function(size) {
     // After the runtime is initialized, we must only use sbrk() normally.
 #if DYLINK_DEBUG
diff --git a/src/library_fetch.js b/src/library_fetch.js
index f2d470ff03f34..5b6ba93dddf8e 100644
--- a/src/library_fetch.js
+++ b/src/library_fetch.js
@@ -28,6 +28,8 @@ var LibraryFetch = {
 
   emscripten_start_fetch: startFetch,
   emscripten_start_fetch__deps: [
+    'malloc',
+    'free',
     '$Fetch',
     '$fetchXHR',
     '$callUserCallback',
diff --git a/src/library_webgl.js b/src/library_webgl.js
index 0350f6236c435..ebf20a9e155e7 100644
--- a/src/library_webgl.js
+++ b/src/library_webgl.js
@@ -152,6 +152,7 @@ var LibraryGL = {
   $GL__deps: [
 #if PTHREADS
     'malloc', // Needed by registerContext
+    'free', // Needed by deleteContext
 #endif
 #if MIN_WEBGL_VERSION == 1
     '$webgl_enable_ANGLE_instanced_arrays',
@@ -3933,7 +3934,7 @@ var LibraryGL = {
     }
   },
 
-  glMapBufferRange__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget'],
+  glMapBufferRange__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget', 'malloc'],
   glMapBufferRange: function(target, offset, length, access) {
     if ((access & (0x1/*GL_MAP_READ_BIT*/ | 0x20/*GL_MAP_UNSYNCHRONIZED_BIT*/)) != 0) {
       err("glMapBufferRange access does not support MAP_READ or MAP_UNSYNCHRONIZED");
@@ -4015,7 +4016,7 @@ var LibraryGL = {
       HEAPU8.subarray(mapping.mem + offset, mapping.mem + offset + length));
   },
 
-  glUnmapBuffer__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget'],
+  glUnmapBuffer__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget', 'free'],
   glUnmapBuffer: function(target) {
     if (!emscriptenWebGLValidateMapBufferTarget(target)) {
       GL.recordError(0x500/*GL_INVALID_ENUM*/);
diff --git a/src/library_webgpu.js b/src/library_webgpu.js
index 47f1954530497..4eeff83cea1ef 100644
--- a/src/library_webgpu.js
+++ b/src/library_webgpu.js
@@ -810,7 +810,7 @@ var LibraryWebGPU = {
     device.label = UTF8ToString(labelPtr);
   },
 
-  wgpuDeviceSetDeviceLostCallback__deps: ['$callUserCallback', '$stringToNewUTF8'],
+  wgpuDeviceSetDeviceLostCallback__deps: ['$callUserCallback', '$stringToNewUTF8', 'free'],
   wgpuDeviceSetDeviceLostCallback: function(deviceId, callback, userdata) {
     var deviceWrapper = WebGPU.mgrDevice.objects[deviceId];
     {{{ gpu.makeCheckDefined('deviceWrapper') }}}
@@ -2432,7 +2432,7 @@ var LibraryWebGPU = {
 #endif
   },
 
-  wgpuInstanceRequestAdapter__deps: ['$callUserCallback', '$stringToNewUTF8'],
+  wgpuInstanceRequestAdapter__deps: ['$callUserCallback', '$stringToNewUTF8', 'free'],
   wgpuInstanceRequestAdapter: function(instanceId, options, callback, userdata) {
     {{{ gpu.makeCheck('instanceId === 0, "WGPUInstance is ignored"') }}}
 
@@ -2513,7 +2513,7 @@ var LibraryWebGPU = {
     return adapter.features.has(WebGPU.FeatureName[featureEnumValue]);
   },
 
-  wgpuAdapterRequestDevice__deps: ['$callUserCallback', '$stringToNewUTF8'],
+  wgpuAdapterRequestDevice__deps: ['$callUserCallback', '$stringToNewUTF8', 'free'],
   wgpuAdapterRequestDevice: function(adapterId, descriptor, callback, userdata) {
     var adapter = WebGPU.mgrAdapter.get(adapterId);
 
diff --git a/test/other/metadce/test_metadce_cxx_mangle.exports b/test/other/metadce/test_metadce_cxx_mangle.exports
index 2a14e2cb1fbda..33a07671bf2f2 100644
--- a/test/other/metadce/test_metadce_cxx_mangle.exports
+++ b/test/other/metadce/test_metadce_cxx_mangle.exports
@@ -16,7 +16,6 @@ dynCall_viijii
 free
 getTempRet0
 main
-malloc
 memory
 setTempRet0
 setThrew
diff --git a/test/other/metadce/test_metadce_cxx_mangle.size b/test/other/metadce/test_metadce_cxx_mangle.size
index 4cc57b2d3f06f..d310455c80818 100644
--- a/test/other/metadce/test_metadce_cxx_mangle.size
+++ b/test/other/metadce/test_metadce_cxx_mangle.size
@@ -1 +1 @@
-219427
+219418
