[lld] 2855525 - [ELF] Handle INCLUDE like a call stack (#193427)

via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 22 19:59:05 PDT 2026


Author: Fangrui Song
Date: 2026-04-22T19:59:00-07:00
New Revision: 2855525c4a1f7c1e105b2b71a3c6b9b34f842f36

URL: https://github.com/llvm/llvm-project/commit/2855525c4a1f7c1e105b2b71a3c6b9b34f842f36
DIFF: https://github.com/llvm/llvm-project/commit/2855525c4a1f7c1e105b2b71a3c6b9b34f842f36.diff

LOG: [ELF] Handle INCLUDE like a call stack (#193427)

The lexer maintains a stack of buffers, which allows a construct
started in an INCLUDE'd file to be closed by the parent. This produces
spurious acceptance of malformed scripts (e.g. a bare assignment with
no trailing `;` in the include, terminated by the parent's `;` after
`INCLUDE`) and undefined-behavior span computations in
`readAssignment`'s `commandString` (issue #190376).

Force each INCLUDE to fully parse its own content, similar to a call
stack frame. `ScriptLexer::lex` no longer auto-pops on EOF; the
`buffers` member is gone. `readInclude` takes a `function_ref<void()>`
callback, and the four call sites (top-level, SECTIONS, output
section, MEMORY) pass a context-appropriate parser.

With this, each buffer contains complete parser structures by
construction, so the `[oldS, curTok)` pointer range in
`readAssignment` no longer needs a guard.

Added: 
    lld/test/ELF/linkerscript/include-mid-construct.s

Modified: 
    lld/ELF/ScriptLexer.cpp
    lld/ELF/ScriptLexer.h
    lld/ELF/ScriptParser.cpp
    lld/test/ELF/linkerscript/memory-include.test
    lld/test/ELF/linkerscript/output-section-include.test
    lld/test/ELF/linkerscript/section-include.test

Removed: 
    


################################################################################
diff  --git a/lld/ELF/ScriptLexer.cpp b/lld/ELF/ScriptLexer.cpp
index c16a70ba1ce8f..3a7207def2c0b 100644
--- a/lld/ELF/ScriptLexer.cpp
+++ b/lld/ELF/ScriptLexer.cpp
@@ -52,14 +52,13 @@ ScriptLexer::Buffer::Buffer(Ctx &ctx, MemoryBufferRef mb)
 }
 
 ScriptLexer::ScriptLexer(Ctx &ctx, MemoryBufferRef mb)
-    : ctx(ctx), curBuf(ctx, mb), mbs(1, mb) {
+    : ctx(ctx), curBuf(ctx, mb) {
   activeFilenames.insert(mb.getBufferIdentifier());
 }
 
 // Returns a whole line containing the current token.
 StringRef ScriptLexer::getLine() {
-  StringRef s = getCurrentMB().getBuffer();
-
+  StringRef s(curBuf.begin, curBuf.s.end() - curBuf.begin);
   size_t pos = s.rfind('\n', prevTok.data() - s.data());
   if (pos != StringRef::npos)
     s = s.substr(pos + 1);
@@ -72,8 +71,7 @@ size_t ScriptLexer::getColumnNumber() {
 }
 
 std::string ScriptLexer::getCurrentLocation() {
-  std::string filename = std::string(getCurrentMB().getBufferIdentifier());
-  return (filename + ":" + Twine(prevTokLine)).str();
+  return (curBuf.filename + ":" + Twine(prevTokLine)).str();
 }
 
 // We don't want to record cascading errors. Keep only the first one.
@@ -93,15 +91,10 @@ void ScriptLexer::lex() {
     StringRef &s = curBuf.s;
     s = skipSpace(s);
     if (s.empty()) {
-      // If this buffer is from an INCLUDE command, switch to the "return
-      // value"; otherwise, mark EOF.
-      if (buffers.empty()) {
-        eof = true;
-        return;
-      }
-      activeFilenames.erase(curBuf.filename);
-      curBuf = buffers.pop_back_val();
-      continue;
+      // If this buffer is from an INCLUDE, the caller is responsible for
+      // popping to the parent buffer.
+      eof = true;
+      return;
     }
     curTokState = lexState;
 
@@ -275,17 +268,3 @@ ScriptLexer::Token ScriptLexer::till(StringRef tok) {
   setError("unexpected EOF");
   return {};
 }
-
-// Returns true if S encloses T.
-static bool encloses(StringRef s, StringRef t) {
-  return s.bytes_begin() <= t.bytes_begin() && t.bytes_end() <= s.bytes_end();
-}
-
-MemoryBufferRef ScriptLexer::getCurrentMB() {
-  // Find input buffer containing the current token.
-  assert(!mbs.empty());
-  for (MemoryBufferRef mb : mbs)
-    if (encloses(mb.getBuffer(), curBuf.s))
-      return mb;
-  llvm_unreachable("getCurrentMB: failed to find a token");
-}

diff  --git a/lld/ELF/ScriptLexer.h b/lld/ELF/ScriptLexer.h
index ba49155b9dc88..5627957ba2f27 100644
--- a/lld/ELF/ScriptLexer.h
+++ b/lld/ELF/ScriptLexer.h
@@ -14,7 +14,6 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/MemoryBufferRef.h"
-#include <vector>
 
 namespace lld::elf {
 struct Ctx;
@@ -34,9 +33,9 @@ class ScriptLexer {
     Buffer(Ctx &ctx, MemoryBufferRef mb);
   };
   Ctx &ctx;
-  // The current buffer and parent buffers due to INCLUDE.
+  // The currently lexed buffer. INCLUDE runs a nested parse on a new `Buffer`,
+  // similar to a call stack frame.
   Buffer curBuf;
-  SmallVector<Buffer, 0> buffers;
 
   // Used to detect INCLUDE() cycles.
   llvm::DenseSet<StringRef> activeFilenames;
@@ -80,9 +79,6 @@ class ScriptLexer {
   void expect(StringRef expect);
   Token till(StringRef tok);
   std::string getCurrentLocation();
-  MemoryBufferRef getCurrentMB();
-
-  std::vector<MemoryBufferRef> mbs;
 
 private:
   StringRef getLine();

diff  --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index 2746193241330..5383f3eed3f8b 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -59,7 +59,7 @@ class ScriptParser final : ScriptLexer {
   void readEntry();
   void readExtern();
   void readGroup();
-  void readInclude();
+  void readInclude(llvm::function_ref<void()> parse);
   void readInput();
   void readLinkerScriptStmt(StringRef tok);
   void readMemory();
@@ -74,6 +74,7 @@ class ScriptParser final : ScriptLexer {
   void readSections();
   void readSectionsStmt(SmallVectorImpl<SectionCommand *> &v, StringRef tok);
   void readOutputSectionStmt(OutputSection &osec, StringRef tok);
+  void readStmts(llvm::function_ref<void(StringRef)> readStmt);
   void readTarget();
   void readVersion();
   void readVersionScriptCommand();
@@ -239,12 +240,7 @@ void ScriptParser::readVersion() {
 }
 
 void ScriptParser::readLinkerScript() {
-  while (!atEOF()) {
-    StringRef tok = next();
-    if (atEOF())
-      break;
-    readLinkerScriptStmt(tok);
-  }
+  readStmts([&](StringRef t) { readLinkerScriptStmt(t); });
 }
 
 void ScriptParser::readLinkerScriptStmt(StringRef tok) {
@@ -258,7 +254,8 @@ void ScriptParser::readLinkerScriptStmt(StringRef tok) {
   } else if (tok == "GROUP") {
     readGroup();
   } else if (tok == "INCLUDE") {
-    readInclude();
+    readInclude(
+        [&] { readStmts([&](StringRef t) { readLinkerScriptStmt(t); }); });
   } else if (tok == "INPUT") {
     readInput();
   } else if (tok == "MEMORY") {
@@ -303,8 +300,7 @@ void ScriptParser::readDefsym() {
   Expr e = readExpr();
   if (!atEOF())
     setError("EOF expected, but got " + next());
-  auto *cmd = make<SymbolAssignment>(
-      name, e, 0, getCurrentMB().getBufferIdentifier().str());
+  auto *cmd = make<SymbolAssignment>(name, e, 0, curBuf.filename.str());
   ctx.script->sectionCommands.push_back(cmd);
 }
 
@@ -346,8 +342,7 @@ void ScriptParser::addFile(StringRef s) {
     ctx.driver.addLibrary(s.substr(2));
   } else {
     // Case 4: s is a relative path. Search in the directory of the script file.
-    std::string filename = std::string(getCurrentMB().getBufferIdentifier());
-    StringRef directory = sys::path::parent_path(filename);
+    StringRef directory = sys::path::parent_path(curBuf.filename);
     if (!directory.empty()) {
       SmallString<0> path(directory);
       sys::path::append(path, s);
@@ -400,22 +395,41 @@ void ScriptParser::readGroup() {
     ++ctx.driver.nextGroupId;
 }
 
-void ScriptParser::readInclude() {
+void ScriptParser::readInclude(llvm::function_ref<void()> parse) {
   StringRef name = readName();
   if (!activeFilenames.insert(name).second) {
     setError("there is a cycle in linker script INCLUDEs");
     return;
   }
 
-  if (std::optional<std::string> path = searchScript(ctx, name)) {
-    if (std::optional<MemoryBufferRef> mb = readFile(ctx, *path)) {
-      buffers.push_back(curBuf);
-      curBuf = Buffer(ctx, *mb);
-      mbs.push_back(*mb);
-    }
+  std::optional<std::string> path = searchScript(ctx, name);
+  if (!path) {
+    setError("cannot find linker script " + name);
+    return;
+  }
+  std::optional<MemoryBufferRef> mb = readFile(ctx, *path);
+  if (!mb)
     return;
+
+  SaveAndRestore savedBuf(curBuf, Buffer(ctx, *mb));
+  SaveAndRestore savedPrevTok(prevTok, StringRef());
+  SaveAndRestore savedPrevTokLine(prevTokLine, size_t(1));
+  parse();
+
+  // parse() leaves `eof` true on normal completion; reset so the parent
+  // buffer continues to be lexed.
+  eof = false;
+  activeFilenames.erase(name);
+}
+
+// Drive `readStmt` on each token until EOF of the current buffer.
+void ScriptParser::readStmts(llvm::function_ref<void(StringRef)> readStmt) {
+  while (!atEOF()) {
+    StringRef tok = next();
+    if (atEOF())
+      return;
+    readStmt(tok);
   }
-  setError("cannot find linker script " + name);
 }
 
 void ScriptParser::readInput() {
@@ -707,7 +721,8 @@ void ScriptParser::readSectionsStmt(SmallVectorImpl<SectionCommand *> &v,
     return;
   }
   if (tok == "INCLUDE") {
-    readInclude();
+    readInclude(
+        [&] { readStmts([&](StringRef t) { readSectionsStmt(v, t); }); });
     return;
   }
 
@@ -1097,7 +1112,9 @@ void ScriptParser::readOutputSectionStmt(OutputSection &osec, StringRef tok) {
   } else if (tok == "SORT") {
     readSort();
   } else if (tok == "INCLUDE") {
-    readInclude();
+    readInclude([&] {
+      readStmts([&](StringRef t) { readOutputSectionStmt(osec, t); });
+    });
   } else if (tok == "(" || tok == ")") {
     setError("expected filename pattern");
   } else if (peek() == "(") {
@@ -1856,7 +1873,7 @@ void ScriptParser::readMemory() {
 
 void ScriptParser::readMemoryStmt(StringRef tok) {
   if (tok == "INCLUDE") {
-    readInclude();
+    readInclude([&] { readStmts([&](StringRef t) { readMemoryStmt(t); }); });
     return;
   }
 

diff  --git a/lld/test/ELF/linkerscript/include-mid-construct.s b/lld/test/ELF/linkerscript/include-mid-construct.s
new file mode 100644
index 0000000000000..951c72fc93860
--- /dev/null
+++ b/lld/test/ELF/linkerscript/include-mid-construct.s
@@ -0,0 +1,18 @@
+# REQUIRES: x86
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+
+## A stray ';' in the parent after INCLUDE cannot complete the inner assignment.
+# RUN: not ld.lld a.o -T top.lds 2>&1 | FileCheck %s --check-prefix=TOP
+# TOP: error: inc-top.lds:1: unexpected EOF
+
+#--- top.lds
+INCLUDE "inc-top.lds";
+
+#--- inc-top.lds
+foo = 1
+
+#--- a.s
+.globl _start
+_start:
+  ret

diff  --git a/lld/test/ELF/linkerscript/memory-include.test b/lld/test/ELF/linkerscript/memory-include.test
index 6c6a89e45787a..2d938d6c7fa93 100644
--- a/lld/test/ELF/linkerscript/memory-include.test
+++ b/lld/test/ELF/linkerscript/memory-include.test
@@ -17,6 +17,17 @@
 # EMPTY:      LOAD {{.*}} 0x0000000000001000 0x0000000000001000 {{.*}} R E
 # EMPTY-NEXT: LOAD {{.*}} 0x0000000000002000 0x0000000000002000 {{.*}} RW
 
+## A region declaration truncated mid-expression cannot be completed by the
+## parent MEMORY { ... }.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
+## A stray '}' in the include cannot close the parent MEMORY { ... }.
+# RUN: cp brace.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=BRACE
+# BRACE: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .section .text,"ax"
 .global _start
@@ -54,3 +65,9 @@ SECTIONS {
 }
 
 #--- inc-empty.lds
+
+#--- trunc.lds
+RAM3 : ORIGIN = 0x4000, LENGTH
+
+#--- brace.lds
+}

diff  --git a/lld/test/ELF/linkerscript/output-section-include.test b/lld/test/ELF/linkerscript/output-section-include.test
index 6cdf07ca7007e..a6392b315870c 100644
--- a/lld/test/ELF/linkerscript/output-section-include.test
+++ b/lld/test/ELF/linkerscript/output-section-include.test
@@ -16,6 +16,12 @@
 # RUN: llvm-objdump --section-headers a.out | FileCheck %s --check-prefix=CHECK2
 # CHECK2: .data         00000010 0000000000002000 DATA
 
+## A BYTE() with an unclosed paren in the include cannot be completed by the
+## parent output-section body.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .section .text,"ax"
 .global _start
@@ -42,3 +48,6 @@ SECTIONS {
 
 #--- full.lds
 QUAD(0)
+
+#--- trunc.lds
+BYTE(42

diff  --git a/lld/test/ELF/linkerscript/section-include.test b/lld/test/ELF/linkerscript/section-include.test
index 16ed19f455d50..88c5f84de0b5f 100644
--- a/lld/test/ELF/linkerscript/section-include.test
+++ b/lld/test/ELF/linkerscript/section-include.test
@@ -19,6 +19,17 @@
 # CHECK2-NEXT: .data2        00000008 0000000000002008 DATA
 # CHECK2-NEXT: .data3        00000008 0000000000002010 DATA
 
+## An unclosed output section in the include cannot be closed by the outer
+## SECTIONS { ... } '}'.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
+## A stray '}' in the include cannot close the parent SECTIONS { ... }.
+# RUN: cp brace.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=BRACE
+# BRACE: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .global _start
 _start: nop
@@ -43,3 +54,9 @@ SECTIONS {
 
 #--- full.lds
 .data2 : { QUAD(0) } > RAM
+
+#--- trunc.lds
+.text : { *(.text*)
+
+#--- brace.lds
+}


        


More information about the llvm-commits mailing list