LLD Silent Bug: Undefined Versioned Symbols

by Square 44 views
Iklan Headers

Hey guys, ever stumbled upon a weird linking issue that just wouldn't quit? Well, buckle up, because we're diving deep into a peculiar behavior with LLD (the LLVM Linker) that might just have you scratching your head. Specifically, we're looking at a situation where LLD silently ignores undefined references to versioned symbols when it finds an unversioned equivalent. Yep, you read that right – no errors, no warnings, just… silence. And as we all know, silence can be deadly when it comes to debugging. Let's break down this head-scratcher and see what's going on. This will help you to understand LLD will silently ignore undefined references to a versioned symbol if there is an unversioned equivalent.

The Setup: Versioned Symbols and Libraries

So, what exactly are we talking about? First, let's get the basics down. We're dealing with symbol versioning, a feature that allows you to manage multiple versions of symbols within a shared library. This is super handy when you're updating a library without breaking compatibility with older programs that depend on it. Think of it like this: you have a function foo() that's part of libfoo.so. Initially, it's version 1.0. Later, you update foo() but want to keep the old version around for older applications. Symbol versioning lets you do that.

Creating libfoo with Versioning

Let's start by creating a versioned libfoo. This is our first step towards reproducing the issue. We define a simple foo() function in foo.c:

// foo.c
void foo(void) {}

Next, we create a version script, libfoo.ver. This script tells the linker which symbols to version and how:

// libfoo.ver
VERSION_1.0 {
    global:
        foo;
    local:
        *;
};

This script declares that foo should be versioned as VERSION_1.0. The global: section specifies that the symbol is available to be used outside the library. The local: section with * makes all other symbols local to the library.

Now, we compile libfoo.so using gcc and the version script:

gcc foo.c -shared -o libfoo.so -Wl,--version-script=libfoo.ver

This command creates a shared library libfoo.so with the foo symbol versioned.

Creating libbar that Depends on libfoo

Next, we create libbar, which depends on libfoo. In bar.c, we declare that it uses the foo() function. Here's bar.c:

// bar.c
void foo(void);
void bar(void) { foo(); }

We compile bar.c into libbar.so, linking against libfoo.so:

gcc bar.c -shared -o libbar.so -lfoo -L.

The -lfoo flag tells the linker to link against libfoo.so, and -L. tells the linker to look in the current directory (.) for libraries.

Creating main that Depends on libbar

Finally, we create a simple executable main.c that calls bar():

// main.c
void bar(void);
int main(void) { bar(); }

We compile main.c, linking it against libbar.so:

gcc main.c -o main -lbar -L.

The Bug: LLD's Unexpected Behavior

Now, here's where things get interesting. Let's recompile libfoo without versioning. This will create an unversioned version of the foo function. This means removing the -Wl,--version-script=libfoo.ver part during the compilation.

gcc foo.c -shared -o libfoo.so

The Expected Behavior: BFD Fails

When you now try to link the main executable using BFD (the GNU linker), you get an expected error. BFD correctly identifies the missing versioned symbol:

gcc main.c -o main -lbar -L. -fuse-ld=bfd
// Output: ld: ./libbar.so: undefined reference to `foo@VERSION_1.0'
// collect2: error: ld returned 1 exit status

BFD throws a linking error, because the libbar.so expects foo@VERSION_1.0, but the new libfoo.so doesn't provide it. This is the correct and desired behavior.

The Problem: LLD Silently Succeeds

Now, the kicker. When you try the exact same thing with LLD:

gcc main.c -o main -lbar -L. -fuse-ld=lld

LLD doesn't report an error. It links successfully! The executable is created without any warnings about the missing versioned symbol. This is the bug: LLD is silently ignoring the missing reference.

This is problematic, because the program may then crash at runtime. This silent failure makes debugging a nightmare. You might spend hours, days even, trying to figure out why your program isn't behaving as expected, only to find out that a crucial versioned symbol is missing.

Deep Dive: Why Does LLD Do This?

So, what's going on under the hood? Why does LLD behave this way? The issue arises from how LLD handles symbol resolution when it finds an unversioned symbol that matches a versioned symbol. LLD seems to prefer the unversioned symbol and silently uses it, even if it doesn't match the version expected by the dependent libraries. This behavior can lead to subtle bugs and unexpected behavior, especially in complex projects.

This can lead to undefined behavior. While LLD successfully creates the executable, the lack of the correct versioned symbol can cause runtime crashes, data corruption, or other unpredictable issues. It's like putting a square peg in a round hole – it fits (in the sense that the linker doesn't complain), but it doesn't work as intended.

Impact and Mitigation

The impact of this bug is significant. It can:

  • Lead to runtime crashes: The most common outcome is that your program will crash when it tries to use the missing symbol.
  • Introduce subtle bugs: The unversioned symbol might have different behavior than the versioned one, leading to unexpected results.
  • Make debugging difficult: The lack of a clear error message from the linker makes it hard to pinpoint the cause of the problem.
  • Break Compatibility: If you're updating your libraries and relying on versioning, this bug can make it difficult to ensure that older programs continue to work as expected.

Mitigating the issue

Here's what you can do to mitigate the impact of this LLD issue.

  • Careful Testing: Thoroughly test your code. It's critical to have comprehensive unit and integration tests.
  • Stick to BFD: Until LLD fixes this, you could use the GNU linker (BFD) for linking shared libraries. This ensures that undefined symbol errors are caught during the linking stage.
  • Code Review: Always review your code thoroughly and make sure that the versioned symbols are correctly used.
  • Update and Patch: Check for updates and patches from the LLD developers. If the problem is fixed, updating the linker is the best way to solve it.

Conclusion

In a nutshell, this LLD issue shows the importance of understanding your tools and the potential pitfalls they might have. While LLD is a powerful linker, its behavior with versioned symbols can lead to subtle but serious bugs. By being aware of this issue, you can take steps to avoid it and build more reliable software. Always remember that a good understanding of the tools you use is the first step towards creating high-quality, bug-free code.