https://bugs.gentoo.org/917089
https://github.com/microsoft/mimalloc/issues/360
https://github.com/rui314/mold/issues/1071
https://github.com/rui314/mold/commit/da3f5dd4ecf4faaba466ba41c7c30ba4f8f73bfd

From da3f5dd4ecf4faaba466ba41c7c30ba4f8f73bfd Mon Sep 17 00:00:00 2001
From: Rui Ueyama <ruiu@cs.stanford.edu>
Date: Sat, 11 Nov 2023 17:59:54 +0900
Subject: [PATCH] Fix --dynamic-list for DSOs

--dynamic-list, --export-dynamic-symbol and --export-dynamic-symbol-list
have different semantics for executables and DSOs. If the output is an
executable, they specify a list of symbols that are to be exported.
If the output is a shared object, they specify the list of symbols that
are to be interposable.

mold havne't implemented the latter semantics. This commit fixes that
issue.

Fixes https://github.com/rui314/mold/issues/1071
---
 elf/cmdline.cc              |  25 +++-----
 elf/linker-script.cc        |  35 ++++++-----
 elf/main.cc                 |  16 -----
 elf/mold.h                  |  17 +++--
 elf/output-chunks.cc        |   8 ++-
 elf/passes.cc               | 122 +++++++++++++++++++++++++++++-------
 test/elf/dynamic-list4.sh   |  44 +++++++++++++
 test/elf/version-script6.sh |   4 +-
 8 files changed, 194 insertions(+), 77 deletions(-)
 create mode 100755 test/elf/dynamic-list4.sh

diff --git a/elf/cmdline.cc b/elf/cmdline.cc
index 6bc13a300..6c06b4b14 100644
--- a/elf/cmdline.cc
+++ b/elf/cmdline.cc
@@ -1104,21 +1104,21 @@ std::vector<std::string> parse_nonpositional_args(Context<E> &ctx) {
     } else if (read_flag("no-keep-memory")) {
     } else if (read_arg("max-cache-size")) {
     } else if (read_arg("version-script")) {
-      // --version-script, --dynamic-list and --export-dynamic-symbol[-list]
-      // are treated as positional arguments even though they are actually not
-      // positional. This is because linker scripts (a positional argument)
-      // can also specify a version script, and it's better to consolidate
-      // parsing in read_input_files. In particular, version scripts can
-      // modify ctx.default_version which we initialize *after* parsing
-      // non-positional args, so the parsing cannot be done right here.
+      // --version-script is treated as positional arguments even though
+      // they are actually not positional. This is because linker scripts
+      // (a positional argument) can also specify a version script, and
+      // it's better to consolidate parsing in read_input_files. In
+      // particular, version scripts can modify ctx.default_version which
+      // we initialize *after* parsing non-positional args, so the parsing
+      // cannot be done right here.
       remaining.push_back("--version-script=" + std::string(arg));
     } else if (read_arg("dynamic-list")) {
       ctx.arg.Bsymbolic = true;
-      remaining.push_back("--dynamic-list=" + std::string(arg));
+      append(ctx.dynamic_list_patterns, parse_dynamic_list(ctx, arg));
     } else if (read_arg("export-dynamic-symbol")) {
-      remaining.push_back("--export-dynamic-symbol=" + std::string(arg));
+      ctx.dynamic_list_patterns.push_back({arg, "<command line>"});
     } else if (read_arg("export-dynamic-symbol-list")) {
-      remaining.push_back("--export-dynamic-symbol-list=" + std::string(arg));
+      append(ctx.dynamic_list_patterns, parse_dynamic_list(ctx, arg));
     } else if (read_flag("as-needed")) {
       remaining.push_back("--as-needed");
     } else if (read_flag("no-as-needed")) {
@@ -1228,11 +1228,6 @@ std::vector<std::string> parse_nonpositional_args(Context<E> &ctx) {
   if (char *env = getenv("MOLD_REPRO"); env && env[0])
     ctx.arg.repro = true;
 
-  if (ctx.arg.shared || ctx.arg.export_dynamic)
-    ctx.default_version = VER_NDX_GLOBAL;
-  else
-    ctx.default_version = VER_NDX_LOCAL;
-
   if (ctx.arg.default_symver) {
     std::string ver = ctx.arg.soname.empty() ?
       filepath(ctx.arg.output).filename().string() : std::string(ctx.arg.soname);
diff --git a/elf/linker-script.cc b/elf/linker-script.cc
index 4bdc19e7c..7ad500bb8 100644
--- a/elf/linker-script.cc
+++ b/elf/linker-script.cc
@@ -312,7 +312,6 @@ read_version_script_commands(Context<E> &ctx, std::span<std::string_view> &tok,
 
     if (tok[0] == "*") {
       ctx.default_version = (is_global ? ver_idx : (u32)VER_NDX_LOCAL);
-      ctx.default_version_from_version_script = true;
     } else if (is_global) {
       ctx.version_patterns.push_back({unquote(tok[0]), current_file<E>->name,
                                       ver_str, ver_idx, is_cpp});
@@ -367,7 +366,9 @@ void parse_version_script(Context<E> &ctx, MappedFile<Context<E>> *mf) {
 }
 
 template <typename E>
-void read_dynamic_list_commands(Context<E> &ctx, std::span<std::string_view> &tok,
+void read_dynamic_list_commands(Context<E> &ctx,
+                                std::vector<DynamicPattern> &result,
+                                std::span<std::string_view> &tok,
                                 bool is_cpp) {
   while (!tok.empty() && tok[0] != "}") {
     if (tok[0] == "extern") {
@@ -376,11 +377,11 @@ void read_dynamic_list_commands(Context<E> &ctx, std::span<std::string_view> &to
       if (!tok.empty() && tok[0] == "\"C\"") {
         tok = tok.subspan(1);
         tok = skip(ctx, tok, "{");
-        read_dynamic_list_commands(ctx, tok, false);
+        read_dynamic_list_commands(ctx, result, tok, false);
       } else {
         tok = skip(ctx, tok, "\"C++\"");
         tok = skip(ctx, tok, "{");
-        read_dynamic_list_commands(ctx, tok, true);
+        read_dynamic_list_commands(ctx, result, tok, true);
       }
 
       tok = skip(ctx, tok, "}");
@@ -388,29 +389,32 @@ void read_dynamic_list_commands(Context<E> &ctx, std::span<std::string_view> &to
       continue;
     }
 
-    if (tok[0] == "*")
-      ctx.default_version = VER_NDX_GLOBAL;
-    else
-      ctx.version_patterns.push_back({unquote(tok[0]), current_file<E>->name,
-                                      "global", VER_NDX_GLOBAL, is_cpp});
-
+    result.push_back({unquote(tok[0]), "", is_cpp});
     tok = skip(ctx, tok.subspan(1), ";");
   }
 }
 
 template <typename E>
-void parse_dynamic_list(Context<E> &ctx, MappedFile<Context<E>> *mf) {
-  current_file<E> = mf;
-  std::vector<std::string_view> vec = tokenize(ctx, mf->get_contents());
+std::vector<DynamicPattern>
+parse_dynamic_list(Context<E> &ctx, std::string_view path) {
+  std::string_view contents =
+    MappedFile<Context<E>>::must_open(ctx, std::string(path))->get_contents();
+  std::vector<std::string_view> vec = tokenize(ctx, contents);
   std::span<std::string_view> tok = vec;
+  std::vector<DynamicPattern> result;
 
   tok = skip(ctx, tok, "{");
-  read_dynamic_list_commands(ctx, tok, false);
+  read_dynamic_list_commands(ctx, result, tok, false);
   tok = skip(ctx, tok, "}");
   tok = skip(ctx, tok, ";");
 
   if (!tok.empty())
     SyntaxError(ctx, tok[0]) << "trailing garbage token";
+
+  for (DynamicPattern &p : result)
+    p.source = path;
+
+  return result;
 }
 
 using E = MOLD_TARGET;
@@ -418,6 +422,7 @@ using E = MOLD_TARGET;
 template void parse_linker_script(Context<E> &, MappedFile<Context<E>> *);
 template std::string_view get_script_output_type(Context<E> &, MappedFile<Context<E>> *);
 template void parse_version_script(Context<E> &, MappedFile<Context<E>> *);
-template void parse_dynamic_list(Context<E> &, MappedFile<Context<E>> *);
+template std::vector<DynamicPattern> parse_dynamic_list(Context<E> &, std::string_view);
+
 
 } // namespace mold::elf
diff --git a/elf/main.cc b/elf/main.cc
index c4f3cd6ff..6df00cfe9 100644
--- a/elf/main.cc
+++ b/elf/main.cc
@@ -299,22 +299,6 @@ static void read_input_files(Context<E> &ctx, std::span<std::string> args) {
       if (!mf)
         Fatal(ctx) << "--version-script: file not found: " << arg;
       parse_version_script(ctx, mf);
-    } else if (remove_prefix(arg, "--dynamic-list=")) {
-      MappedFile<Context<E>> *mf = find_from_search_paths(ctx, std::string(arg));
-      if (!mf)
-        Fatal(ctx) << "--dynamic-list: file not found: " << arg;
-      parse_dynamic_list(ctx, mf);
-    } else if (remove_prefix(arg, "--export-dynamic-symbol=")) {
-      if (arg == "*")
-        ctx.default_version = VER_NDX_GLOBAL;
-      else
-        ctx.version_patterns.push_back({arg, "--export-dynamic-symbol",
-                                        "global", VER_NDX_GLOBAL, false});
-    } else if (remove_prefix(arg, "--export-dynamic-symbol-list=")) {
-      MappedFile<Context<E>> *mf = find_from_search_paths(ctx, std::string(arg));
-      if (!mf)
-        Fatal(ctx) << "--export-dynamic-symbol-list: file not found: " << arg;
-      parse_dynamic_list(ctx, mf);
     } else if (arg == "--push-state") {
       state.push_back({ctx.as_needed, ctx.whole_archive, ctx.is_static,
                        ctx.in_lib});
diff --git a/elf/mold.h b/elf/mold.h
index 7ff7c2e6f..d593f6840 100644
--- a/elf/mold.h
+++ b/elf/mold.h
@@ -1281,8 +1281,15 @@ get_script_output_type(Context<E> &ctx, MappedFile<Context<E>> *mf);
 template <typename E>
 void parse_version_script(Context<E> &ctx, MappedFile<Context<E>> *mf);
 
+struct DynamicPattern {
+  std::string_view pattern;
+  std::string_view source;
+  bool is_cpp = false;
+};
+
 template <typename E>
-void parse_dynamic_list(Context<E> &ctx, MappedFile<Context<E>> *mf);
+std::vector<DynamicPattern>
+parse_dynamic_list(Context<E> &ctx, std::string_view path);
 
 //
 // lto.cc
@@ -1733,13 +1740,11 @@ struct Context {
   } arg;
 
   std::vector<VersionPattern> version_patterns;
-  u16 default_version = VER_NDX_GLOBAL;
+  std::vector<DynamicPattern> dynamic_list_patterns;
+  i64 default_version = -1;
   i64 page_size = E::page_size;
   std::optional<int> global_lock_fd;
 
-  // true if default_version is set by a wildcard in version script.
-  bool default_version_from_version_script = false;
-
   // Reader context
   bool as_needed = false;
   bool whole_archive = false;
@@ -2034,7 +2039,7 @@ class Symbol {
   i32 sym_idx = -1;
 
   i32 aux_idx = -1;
-  u16 ver_idx = 0;
+  i32 ver_idx = -1;
 
   // `flags` has NEEDS_ flags.
   Atomic<u8> flags = 0;
diff --git a/elf/output-chunks.cc b/elf/output-chunks.cc
index f44d448ac..00d5538df 100644
--- a/elf/output-chunks.cc
+++ b/elf/output-chunks.cc
@@ -2550,8 +2550,12 @@ void VerdefSection<E>::construct(Context<E> &ctx) {
   for (std::string_view verstr : ctx.arg.version_definitions)
     write(verstr, idx++, 0);
 
-  for (Symbol<E> *sym : std::span<Symbol<E> *>(ctx.dynsym->symbols).subspan(1))
-    ctx.versym->contents[sym->get_dynsym_idx(ctx)] = sym->ver_idx;
+  for (Symbol<E> *sym : std::span<Symbol<E> *>(ctx.dynsym->symbols).subspan(1)) {
+    i64 ver = sym->ver_idx;
+    if (ver == -1)
+      ver = VER_NDX_GLOBAL;
+    ctx.versym->contents[sym->get_dynsym_idx(ctx)] = ver;
+  }
 }
 
 template <typename E>
diff --git a/elf/passes.cc b/elf/passes.cc
index c6ee0f66b..8c7d5d0f5 100644
--- a/elf/passes.cc
+++ b/elf/passes.cc
@@ -1612,9 +1612,6 @@ template <typename E>
 void apply_version_script(Context<E> &ctx) {
   Timer t(ctx, "apply_version_script");
 
-  // If all patterns are simple (i.e. not containing any meta-
-  // characters and is not a C++ name), we can simply look up
-  // symbols.
   auto is_simple = [&] {
     for (VersionPattern &v : ctx.version_patterns)
       if (v.is_cpp || v.pattern.find_first_of("*?[") != v.pattern.npos)
@@ -1622,6 +1619,9 @@ void apply_version_script(Context<E> &ctx) {
     return true;
   };
 
+  // If all patterns are simple (i.e. not containing any meta-
+  // characters and is not a C++ name), we can simply look up
+  // symbols.
   if (is_simple()) {
     for (VersionPattern &v : ctx.version_patterns) {
       Symbol<E> *sym = get_symbol(ctx, v.pattern);
@@ -1747,44 +1747,124 @@ void compute_import_export(Context<E> &ctx) {
   if (!ctx.arg.shared) {
     tbb::parallel_for_each(ctx.dsos, [&](SharedFile<E> *file) {
       for (Symbol<E> *sym : file->symbols) {
-        if (sym->file && !sym->file->is_dso && sym->visibility != STV_HIDDEN) {
-          if (sym->ver_idx != VER_NDX_LOCAL ||
-              !ctx.default_version_from_version_script) {
-            std::scoped_lock lock(sym->mu);
-            sym->is_exported = true;
-          }
+        if (sym->file && !sym->file->is_dso && sym->visibility != STV_HIDDEN &&
+            sym->ver_idx != VER_NDX_LOCAL) {
+          std::scoped_lock lock(sym->mu);
+          sym->is_exported = true;
         }
       }
     });
   }
 
+  auto should_export = [&](Symbol<E> &sym) {
+    if (sym.visibility == STV_HIDDEN)
+      return false;
+
+    switch (sym.ver_idx) {
+    case -1:
+      if (ctx.arg.shared)
+        return !((ObjectFile<E> *)sym.file)->exclude_libs;
+      return ctx.arg.export_dynamic;
+    case VER_NDX_LOCAL:
+      return false;
+    default:
+      return true;
+    }
+  };
+
   // Export symbols that are not hidden or marked as local.
   // We also want to mark imported symbols as such.
   tbb::parallel_for_each(ctx.objs, [&](ObjectFile<E> *file) {
     for (Symbol<E> *sym : file->get_global_syms()) {
-      if (!sym->file || sym->visibility == STV_HIDDEN ||
-          sym->ver_idx == VER_NDX_LOCAL)
-        continue;
-
-      // If we are using a symbol in a DSO, we need to import it at runtime.
-      if (sym->file != file && sym->file->is_dso && !sym->is_absolute()) {
-        std::scoped_lock lock(sym->mu);
-        sym->is_imported = true;
+      // If we are using a symbol in a DSO, we need to import it.
+      if (sym->file && sym->file->is_dso) {
+        if (!sym->is_absolute()) {
+          std::scoped_lock lock(sym->mu);
+          sym->is_imported = true;
+        }
         continue;
       }
 
-      // If we are creating a DSO, all global symbols are exported by default.
-      if (sym->file == file) {
-        std::scoped_lock lock(sym->mu);
+      // If we have a definition of a symbol, we may want to export it.
+      if (sym->file == file && should_export(*sym)) {
         sym->is_exported = true;
 
-        if (ctx.arg.shared && sym->visibility != STV_PROTECTED &&
+        // Exported symbols are marked as imported as well by default
+        // for DSOs.
+        if (ctx.arg.shared &&
+            sym->visibility != STV_PROTECTED &&
             !ctx.arg.Bsymbolic &&
             !(ctx.arg.Bsymbolic_functions && sym->get_type() == STT_FUNC))
           sym->is_imported = true;
       }
     }
   });
+
+
+  // Apply --dynamic-list, --export-dynamic-symbol and
+  // --export-dynamic-symbol-list options.
+  //
+  // The semantics of these options vary depending on whether we are
+  // creating an executalbe or a shared object.
+  //
+  // For executable, matched symbols are exported.
+  //
+  // For shared objects, matched symbols are imported if it is already
+  // exported so that they are interposable. In other words, symbols
+  // that did not match will be bound locally within the output file,
+  // effectively turning them into protected symbols.
+  MultiGlob matcher;
+  MultiGlob cpp_matcher;
+
+  auto handle_match = [&](Symbol<E> *sym) {
+    if (ctx.arg.shared) {
+      if (sym->is_exported)
+        sym->is_imported = true;
+    } else {
+      if (sym->file && !sym->file->is_dso && sym->visibility != STV_HIDDEN)
+        sym->is_exported = true;
+    }
+  };
+
+  for (DynamicPattern &p : ctx.dynamic_list_patterns) {
+    if (p.is_cpp) {
+      if (!cpp_matcher.add(p.pattern, 1))
+        Fatal(ctx) << p.source << ": invalid dynamic list entry: "
+                   << p.pattern;
+      continue;
+    }
+
+    if (p.pattern.find_first_of("*?[") != p.pattern.npos) {
+      if (!matcher.add(p.pattern, 1))
+        Fatal(ctx) << p.source << ": invalid dynamic list entry: "
+                   << p.pattern;
+      continue;
+    }
+
+    handle_match(get_symbol(ctx, p.pattern));
+  }
+
+  if (!matcher.empty() || !cpp_matcher.empty()) {
+    tbb::parallel_for_each(ctx.objs, [&](ObjectFile<E> *file) {
+      for (Symbol<E> *sym : file->get_global_syms()) {
+        if (sym->file != file)
+          continue;
+        if (ctx.arg.shared && !sym->is_exported)
+          continue;
+
+        std::string_view name = sym->name();
+
+        if (matcher.find(name)) {
+          handle_match(sym);
+        } else if (!cpp_matcher.empty()) {
+          if (std::optional<std::string_view> s = cpp_demangle(name))
+            name = *s;
+          if (cpp_matcher.find(name))
+            handle_match(sym);
+        }
+      }
+    });
+  }
 }
 
 // Compute the "address-taken" bit for each input section.
diff --git a/test/elf/dynamic-list4.sh b/test/elf/dynamic-list4.sh
new file mode 100755
index 000000000..83d88887e
--- /dev/null
+++ b/test/elf/dynamic-list4.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+. $(dirname $0)/common.inc
+
+cat <<EOF | $CC -o $t/a.o -c -xc - -fPIC
+#include <stdio.h>
+
+void foo() { printf("foo1 "); }
+void bar() { printf("bar1 "); }
+void baz() { printf("baz1 "); }
+
+void print() {
+  foo();
+  bar();
+  baz();
+  printf("\n");
+}
+EOF
+
+cat <<EOF > $t/dyn
+{ foo; bar; };
+EOF
+
+$CC -B. -shared -o $t/b.so $t/a.o -Wl,--dynamic-list=$t/dyn
+
+cat <<EOF | $CC -o $t/c.o -c -xc - -fPIC
+#include <stdio.h>
+void foo() { printf("foo2 "); }
+void bar() { printf("bar2 "); }
+void baz() { printf("baz2 "); }
+EOF
+
+$CC -B. -shared -o $t/d.so $t/c.o
+
+cat <<EOF | $CC -o $t/e.o -c -xc -
+#include <stdio.h>
+void print();
+int main() { print(); }
+EOF
+
+$CC -B. -o $t/exe1 $t/e.o -Wl,-push-state,-no-as-needed $t/b.so -Wl,-pop-state
+$QEMU $t/exe1 | grep -q 'foo1 bar1 baz1'
+
+$CC -B. -o $t/exe2 $t/e.o -Wl,-push-state,-no-as-needed $t/d.so $t/b.so -Wl,-pop-state
+$QEMU $t/exe2 | grep -q 'foo2 bar2 baz1'
diff --git a/test/elf/version-script6.sh b/test/elf/version-script6.sh
index 74e2f9a89..44f809ef3 100755
--- a/test/elf/version-script6.sh
+++ b/test/elf/version-script6.sh
@@ -9,10 +9,10 @@ EOF
 cat <<EOF | $CXX -fPIC -c -o $t/b.o -xc -
 int foo = 5;
 int bar = 6;
+int quux = 100;
 EOF
 
-$CC -B. -shared -Wl,--version-script=$t/a.ver \
-  -o $t/c.so $t/b.o
+$CC -B. -shared -Wl,--version-script=$t/a.ver -o $t/c.so $t/b.o
 
 cat <<'EOF' > $t/d.ver
 VER_Y1 { local; *; };
