Skip to content

[6.18.40] ld.lld: error: undefined symbol: __scoped_seqlock_bug #2178

Description

@ms178

Hi Nick and Nathan!

1. Environment / Toolchain Info

  • Kernel Version: v6.18.40
  • Compiler: 24.0.0git
  • Linker: 24.0.0
  • Build Configuration: CONFIG_LTO_CLANG_FULL=y, with highly optimizing compiler flags.

2. Symptom / Link Error

With 6.18.40, LTO and my llvm-24-git toolchain, I started to see the following undefined symbol error which was not present in 6.18.39:

ld.lld: error: undefined symbol: __scoped_seqlock_bug
>>> referenced by seqlock.h:1257 (./include/linux/seqlock.h:1257)
>>>               vmlinux.o:(__scoped_seqlock_next)
make[1]: *** [Makefile:1292: vmlinux] Error 2

The next sections rely on analysis and proposals by an AI agent. The usual AI caveats apply. It may help to speed up the debugging process though. I can confirm that the proposed fix fixed the issue for me.

3. Root-Cause Analysis

Code Walkthrough

In include/linux/seqlock.h, the scoped seqlock macro is defined as:

#define __scoped_seqlock_read(_seqlock, _target, _s)             \
	for (struct ss_tmp _s __cleanup(__scoped_seqlock_cleanup) =  \
	     { .state = ss_lockless, .data = read_seqbegin(_seqlock) }; \
	     _s.state != ss_done;                                    \
	     __scoped_seqlock_next(&_s, _seqlock, _target))

The loop condition is explicitly _s.state != ss_done. Therefore, when the loop step expression __scoped_seqlock_next() is called, _s.state can never be ss_done.

The helper __scoped_seqlock_next() implements this switch:

static inline void
__scoped_seqlock_next(struct ss_tmp *sst, seqlock_t *lock, enum ss_state target)
{
	switch (sst->state) {
	case ss_done:
		__scoped_seqlock_bug();   // <--- Line 1257: triggers the bug
		return;
    ...

To act as an optimization assertion/canary, __scoped_seqlock_bug() is declared without a definition on modern compilers:

#if defined(CONFIG_CC_IS_GCC) && CONFIG_GCC_VERSION < 90000
static inline void __scoped_seqlock_bug(void) { }
#else
extern void __scoped_seqlock_bug(void);   // <--- Undefined link-time assert
#endif

Compiler Optimization Failure

When compiling complex functions that heavily inline scoped_seqlock_read blocks (such as hot paths inside the scheduler or network drivers), the nested control-flow graphs, loop unrolling, or inlining limits prevent Clang’s SimplifyCFG or loop optimization passes from statically proving that the ss_done case in the switch is unreachable.

Consequently, the compiler fails to prune the case ss_done branch. Instead of optimizing it out, Clang is forced to emit a real call to the undefined extern void __scoped_seqlock_bug(void);, which successfully passes compilation but violates link-time constraints under LLD.


4. Upstream Reproduction (Vanilla 6.18.40)

This is not caused by out-of-tree or custom patches. You can reproduce this on vanilla 6.18.40 by enabling Clang LTO and compiling files that contain heavily nested or complex inline blocks using scoped seqlocks.


5. Proposed Fix

Rather than relying on an undefined external function (which turns compiler analysis limitations into fatal link errors), we should use __builtin_unreachable(). This provides a definitive, non-negotiable optimization hint to the compiler, guaranteeing that the unreachable branch is pruned and no symbol reference is generated:

diff --git a/include/linux/seqlock.h b/include/linux/seqlock.h
index bdc30dfb3..4cb5e7c81 100644
--- a/include/linux/seqlock.h
+++ b/include/linux/seqlock.h
@@ -1243,7 +1243,7 @@ static inline void __scoped_seqlock_bug(void) { }
 #else
 /*
  * Canary for compiler optimization -- if the compiler doesn't realize this is
- * an impossible state, it very likely generates sub-optimal code here.
+ * an impossible state, use __builtin_unreachable() to safely prune the branch.
  */
-extern void __scoped_seqlock_bug(void);
+static inline void __scoped_seqlock_bug(void) { __builtin_unreachable(); }
 #endif

Why this is the best technical approach:

  1. Link Safety: It eliminates the fragile "undefined symbol" linker block entirely.
  2. Optimal Code Generation: __builtin_unreachable() instructs Clang's optimizer that the path is mathematically impossible. This allows the compiler to prune the dead branch completely, improving branch-prediction accuracy, reducing code footprint, and generating ultra-performant, branchless assembly in the read-side seqlock loops.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs BackportShould be backported to either linux-stable tree or latest llvm release branch.[FEATURE] AutoFDORelated to building the kernel with Automatic Feedback Directed Optimization[FIXED][LINUX] 6.19This bug was fixed in Linux 6.19

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions