[lld] [ELF] Add VersionNode lexer state for better version script parsing (PR #174530)

via llvm-commits llvm-commits at lists.llvm.org
Mon Jan 5 22:24:08 PST 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld-elf

Author: Fangrui Song (MaskRay)

<details>
<summary>Changes</summary>

... so that `local:*;` will be lexed as three tokens instead of a single
one in a version node. This is used by both version scripts and dynamic
lists. Fix #<!-- -->174363

In addition, clean up special code for space-separated `local :` and `global :`.

This patch brings our lexer behavior closer to GNU ld. While GNU ld
additionally rejects more characters like `~/+,=`, we don't implement
this additional validation.


---
Full diff: https://github.com/llvm/llvm-project/pull/174530.diff


6 Files Affected:

- (modified) lld/ELF/ScriptLexer.cpp (+38-14) 
- (modified) lld/ELF/ScriptLexer.h (+2) 
- (modified) lld/ELF/ScriptParser.cpp (+4-2) 
- (modified) lld/test/ELF/dynamic-list-extern.s (+10) 
- (modified) lld/test/ELF/linkerscript/version-script.s (+2-1) 
- (modified) lld/test/ELF/version-script.s (+2-1) 


``````````diff
diff --git a/lld/ELF/ScriptLexer.cpp b/lld/ELF/ScriptLexer.cpp
index 3fa473882f3b4..990e345a69498 100644
--- a/lld/ELF/ScriptLexer.cpp
+++ b/lld/ELF/ScriptLexer.cpp
@@ -124,29 +124,37 @@ void ScriptLexer::lex() {
       return;
     }
 
-    // Some operators form separate tokens.
-    if (s.starts_with("<<=") || s.starts_with(">>=")) {
-      curTok = s.substr(0, 3);
-      s = s.substr(3);
-      return;
-    }
-    if (s.size() > 1 && (s[1] == '=' && strchr("+-*/!&^|", s[0]))) {
-      curTok = s.substr(0, 2);
-      s = s.substr(2);
-      return;
-    }
+    // In Script and Expr states, recognize compound assignment operators.
+    auto doAssign = [&]() -> std::optional<StringRef> {
+      if (s.starts_with("<<=") || s.starts_with(">>=")) {
+        curTok = s.substr(0, 3);
+        s = s.substr(3);
+        return s;
+      }
+      if (s.size() > 1 && (s[1] == '=' && strchr("+-*/!&^|", s[0]))) {
+        curTok = s.substr(0, 2);
+        s = s.substr(2);
+        return s;
+      }
+      return {};
+    };
 
     // Unquoted token. The non-expression token is more relaxed than tokens in
     // C-like languages, so that you can write "file-name.cpp" as one bare
     // token.
     size_t pos;
+    constexpr StringRef scriptChars =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+        "0123456789_.$/\\~=+[]*?-!^:";
     switch (lexState) {
     case State::Script:
-      pos = s.find_first_not_of(
-          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-          "0123456789_.$/\\~=+[]*?-!^:");
+      if (auto ret = doAssign())
+        return;
+      pos = s.find_first_not_of(scriptChars);
       break;
     case State::Expr:
+      if (auto ret = doAssign())
+        return;
       pos = s.find_first_not_of(
           "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
           "0123456789_.$");
@@ -155,6 +163,22 @@ void ScriptLexer::lex() {
            is_contained({"==", "!=", "<=", ">=", "<<", ">>"}, s.substr(0, 2))))
         pos = 2;
       break;
+    case State::VersionNode:
+      // Treat `:` as a token boundary unless it's part of a scope operator `::`
+      // (for extern "C++"). This behavior resembles GNU ld and allows proper
+      // tokenization of patterns like `local:*`.
+      pos = 0;
+      for (; pos != s.size(); ++pos) {
+        if (s[pos] == ':') {
+          if (pos + 1 != s.size() && s[pos + 1] == ':') {
+            ++pos;
+            continue;
+          }
+        } else if (scriptChars.contains(s[pos]))
+          continue;
+        break;
+      }
+      break;
     }
 
     if (pos == 0)
diff --git a/lld/ELF/ScriptLexer.h b/lld/ELF/ScriptLexer.h
index be691022f5385..ba49155b9dc88 100644
--- a/lld/ELF/ScriptLexer.h
+++ b/lld/ELF/ScriptLexer.h
@@ -44,6 +44,8 @@ class ScriptLexer {
   enum class State {
     Script,
     Expr,
+    // Used by version node and dynamic list parsing.
+    VersionNode,
   };
 
   struct Token {
diff --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index b61dc647401a3..07f3f786d7cec 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -179,6 +179,7 @@ static ExprValue bitOr(LinkerScript &s, ExprValue a, ExprValue b) {
 }
 
 void ScriptParser::readDynamicList() {
+  SaveAndRestore saved(lexState, State::VersionNode);
   expect("{");
   SmallVector<SymbolVersion, 0> locals;
   SmallVector<SymbolVersion, 0> globals;
@@ -207,6 +208,7 @@ void ScriptParser::readVersionScript() {
 }
 
 void ScriptParser::readVersionScriptCommand() {
+  SaveAndRestore saved(lexState, State::VersionNode);
   if (consume("{")) {
     readAnonymousDeclaration();
     return;
@@ -1779,11 +1781,11 @@ ScriptParser::readSymbols() {
       SmallVector<SymbolVersion, 0> ext = readVersionExtern();
       v->insert(v->end(), ext.begin(), ext.end());
     } else {
-      if (tok == "local:" || (tok == "local" && consume(":"))) {
+      if (tok == "local" && consume(":")) {
         v = &locals;
         continue;
       }
-      if (tok == "global:" || (tok == "global" && consume(":"))) {
+      if (tok == "global" && consume(":")) {
         v = &globals;
         continue;
       }
diff --git a/lld/test/ELF/dynamic-list-extern.s b/lld/test/ELF/dynamic-list-extern.s
index bb06cebf5f520..04e183763305d 100644
--- a/lld/test/ELF/dynamic-list-extern.s
+++ b/lld/test/ELF/dynamic-list-extern.s
@@ -2,6 +2,7 @@
 
 # Test that we can parse multiple externs.
 
+# RUN: rm -rf %t && mkdir %t && cd %t
 # RUN: llvm-mc -filetype=obj -triple=x86_64-pc-linux %s -o %t.o
 
 # RUN: echo '{ extern "C" { foo; }; extern "C++" { bar; }; };' > %t.list
@@ -9,3 +10,12 @@
 
 # RUN: echo '{ extern "C" { foo }; extern "C++" { bar }; };' > %t.list
 # RUN: ld.lld --dynamic-list %t.list %t.o -shared -o %t.so
+
+# RUN: echo '{ extern "C++" { std::foo; }; };' > %t.list
+# RUN: ld.lld --dynamic-list %t.list %t.o -shared -o %t.so
+
+# RUN: echo '{ extern "C++" { std:foo; }; };' > a.list
+# RUN: not ld.lld --dynamic-list a.list %t.o -shared 2>&1 | FileCheck %s --check-prefix=ERR-COLON
+# RUN: echo '{ extern "C++" { std:::foo; }; };' > a.list
+# RUN: not ld.lld --dynamic-list a.list %t.o -shared 2>&1 | FileCheck %s --check-prefix=ERR-COLON
+# ERR-COLON: error: a.list:1: ; expected, but got :
diff --git a/lld/test/ELF/linkerscript/version-script.s b/lld/test/ELF/linkerscript/version-script.s
index 6b97fede00c37..22ea243648114 100644
--- a/lld/test/ELF/linkerscript/version-script.s
+++ b/lld/test/ELF/linkerscript/version-script.s
@@ -6,7 +6,8 @@
 # RUN: llvm-readobj -V %t.so | FileCheck %s
 
 # RUN: echo "SECTIONS { .text : { bar = foo; *(.text) } }" > %t.script
-# RUN: echo "VERSION { V { global: foo; bar; local: *; }; }" >> %t.script
+## `:` in `local:*` is lexed as a separate token.
+# RUN: echo "VERSION { V { global: foo; bar; local:*; }; }" >> %t.script
 # RUN: ld.lld -T %t.script -shared --no-undefined-version %t.o -o %t.so
 # RUN: llvm-readobj -V %t.so | FileCheck %s
 
diff --git a/lld/test/ELF/version-script.s b/lld/test/ELF/version-script.s
index 7fd3b3733625e..1e8f70bd501a4 100644
--- a/lld/test/ELF/version-script.s
+++ b/lld/test/ELF/version-script.s
@@ -8,8 +8,9 @@
 # RUN: ld.lld --version-script %t.script -shared %t.o %t2.so -o %t.so --fatal-warnings
 # RUN: llvm-readelf --dyn-syms %t.so | FileCheck --check-prefix=DSO %s
 
+## `:` in `local:*` is lexed as a separate token.
 # RUN: echo "# comment" > %t3.script
-# RUN: echo "{ local: *; # comment" >> %t3.script
+# RUN: echo "{ local:*; # comment" >> %t3.script
 # RUN: echo -n "}; # comment" >> %t3.script
 # RUN: ld.lld --version-script %t3.script -shared %t.o %t2.so -o %t3.so
 # RUN: llvm-readelf --dyn-syms %t3.so | FileCheck --check-prefix=DSO2 %s

``````````

</details>


https://github.com/llvm/llvm-project/pull/174530


More information about the llvm-commits mailing list