Support for alternate linkers for Darwin Toolchain (via `-use-ld`)

Hey there,

TL;DR - I was exploring adding the undefined behavior sanitizer support to SPM. To accomplish this we need to be able to specify clang++ vs ld as the linker which doesn't seem supported in Darwin environments. Below I try to walk through how I got to this request and different things I've tried along the way.

I was exploring adding support for the Undefined Behavior sanitizer to Swift Package Manager to complement the existing address and thread sanitizer support. It's been fun exploring llbuild, spm and the swift driver but I think I'm at the point where I would like guidance from the community.

Here is how we got here:

When reviewing the LLVM UBSan documentation, and came across the requirement that clang++ must be used for linking vs ld.

From the docs:

Use clang++ to compile and link your program with -fsanitize=undefined flag. Make sure to use clang++ (not ld) as a linker, so that your executable is linked with proper UBSan runtime libraries. You can use clang instead of clang++ if you’re compiling/linking C code.

I began exploring specifying clang++ when generating the llbuild manifest but that quickly seemed like it wasn't the right path forward since the majority of the other flags were specific to swiftc.

After that I started looking into the flags available I could pass to swiftc (via -Xswiftc) and came across -use-ld.

  -use-ld=<value>         Specifies the linker to be used

This seemed like the right path forward so I started trying to change the linker by passing a statement like swift test -Xlinker -fsantize=undefined -use-ld=clang++ from SPM. Even after passing the proper flags, ld was still used for linking which lead me to start looking into the driver source code to see how -use-ld was handled.

Linkers are specified as part of the ToolChain interface in the Swift Driver.

The Unix Toolchain utilizes this flag and specifies clang++ as the default linker

  if (!Linker.empty()) {
#if defined(__HAIKU__)
    // For now, passing -fuse-ld on Haiku doesn't work as swiftc doesn't
    // recognise it. Passing -use-ld= as the argument works fine.
    Arguments.push_back(context.Args.MakeArgString("-use-ld=" + Linker));
    Arguments.push_back(context.Args.MakeArgString("-fuse-ld=" + Linker));

Since I'm encountering this issue on macOS, I referenced the Darwin toolchain to see if it was handling the flag as well. The Darwin toolchain seems to ignore this flag and only use ld, there is support for specifying a toolchain path but it will also only look for ld there.

// Configure the toolchain.
  // By default, use the system `ld` to link.
  const char *LD = "ld";
  if (const Arg *A = context.Args.getLastArg(options::OPT_tools_directory)) {
    StringRef toolchainPath(A->getValue());

    // If there is a 'ld' in the toolchain folder, use that instead.
    if (auto toolchainLD =
            llvm::sys::findProgramByName("ld", {toolchainPath})) {
      LD = context.Args.MakeArgString(toolchainLD.get());

I started to add support for -use-ld in the Darwin toolchain but stopped when I came across this comment.

  // FIXME: If we used Clang as a linker instead of going straight to ld,
  // we wouldn't have to replicate a bunch of Clang's logic here.

  // Always link the regular compiler_rt if it's present.
  // Note: Normally we'd just add this unconditionally, but it's valid to build
  // Swift and use it as a linker without building compiler_rt.

I'm unsure about this history of this comment or why clang wasn't used originally so it would be good to get more context behind this statement. I'd be happy to work on this change but would possibly need guidance around any linker flags that could not be used directly with clang/clang++.

Last but not least, I wanted to know if this change would be welcome by the community.

Thanks for reading :slight_smile: !

/cc @jrose @Aciid

1 Like

It looks like we can make it work by linking with the required library by hand if the Swift driver change is not possible:

$ swift build -Xcc -fsanitize=undefined -Xlinker -L$(dirname $(xcrun --find swift))/../lib/swift/clang/lib/darwin -Xlinker -lclang_rt.ubsan_osx_dynamic

$ LD_LIBRARY_PATH=$(dirname $(xcrun --find swift))/../lib/swift/clang/lib/darwin ./.build/x86_64-apple-macosx10.10/debug/foo
/private/tmp/foo/Sources/foo/main.c:3:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

Thanks @Aciid!

If we go with this approach of linking the dylib directly I believe we could simplify this requirement by adding support for -santitize=undefined to the Driver, adding undefined to the list of Sanitizers and linking the library directly.

It would require a couple small changes.

Updates to Darwin Toolchain (and likely unix, windows)

diff --git a/lib/Driver/DarwinToolChains.cpp b/lib/Driver/DarwinToolChains.cpp
--- a/lib/Driver/DarwinToolChains.cpp
+++ b/lib/Driver/DarwinToolChains.cpp
+  if (context.OI.SelectedSanitizers & SanitizerKind::Undefined)
+    addLinkSanitizerLibArgsForDarwin(context.Args, Arguments, "undefined", *this);

Adding a new sanitizer type

--- a/lib/Option/SanitizerOptions.cpp
+++ b/lib/Option/SanitizerOptions.cpp
@@ -37,6 +37,8 @@ static StringRef toStringRef(const SanitizerKind kind) {
     return "thread";
   case SanitizerKind::Fuzzer:
     return "fuzzer";
+  case SanitizerKind::Undefined:
+    return "undefined";
   llvm_unreachable("Unsupported sanitizer");

Right, and that would be inline with support for thread and address sanitizers in the Swift driver. @jrose can probably clarify if that approach makes sense.

I think it does. It's a little funny because Swift itself doesn't use UBSan (at least not today), but you can still get benefit from the Clang part. @George_Karpenkov, what do you think?

By the way, the "alternate linker" you pick still has to be something with the same command line interface as ld, so -use-ld=clang++ isn't ever going to work. We don't have a way to talk to Clang-as-linker on Linux either, at least not yet.

Yeah sure, adding this to the driver makes sense.

Ok great! I have a patch locally with changes for SPM as well as the Darwin toolchain to support --sanitize=undefined.

For a full PR I'm assuming we need to:

  • Add support to other toolchains (Unix, Windows)
  • Add equivalent tests to what we use for tsan, asan

Anything else I should do (or not do)?

That sounds like enough to me. Please tag me and George on the compiler PR!

1 Like

Thanks! PR is here

1 Like