Skip to content

[lld][MachO] Support for -interposable #131813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 28, 2025
Merged

[lld][MachO] Support for -interposable #131813

merged 1 commit into from
Mar 28, 2025

Conversation

johnno1962
Copy link
Contributor

As discussed in #53680, add support for ld64's -interposable flag on Apple platforms to lld.

Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Mar 18, 2025

@llvm/pr-subscribers-lld

@llvm/pr-subscribers-lld-macho

Author: John Holdsworth (johnno1962)

Changes

As discussed in #53680, add support for ld64's -interposable flag on Apple platforms to lld.


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

4 Files Affected:

  • (modified) lld/MachO/Config.h (+1)
  • (modified) lld/MachO/Driver.cpp (+1)
  • (modified) lld/MachO/Options.td (-1)
  • (modified) lld/MachO/SymbolTable.cpp (+3-2)
diff --git a/lld/MachO/Config.h b/lld/MachO/Config.h
index f8dcc84e4ee1b..629fa2bfd0cb3 100644
--- a/lld/MachO/Config.h
+++ b/lld/MachO/Config.h
@@ -183,6 +183,7 @@ struct Configuration {
   bool deadStripDylibs = false;
   bool demangle = false;
   bool deadStrip = false;
+  bool interposable = false;
   bool errorForArchMismatch = false;
   bool ignoreAutoLink = false;
   // ld64 allows invalid auto link options as long as the link succeeds. LLD
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 4f6c9b4ddc798..a335b5750b3de 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -1676,6 +1676,7 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
 
   // Must be set before any InputSections and Symbols are created.
   config->deadStrip = args.hasArg(OPT_dead_strip);
+  config->interposable = args.hasArg(OPT_interposable);
 
   config->systemLibraryRoots = getSystemLibraryRoots(args);
   if (const char *path = getReproduceOption(args)) {
diff --git a/lld/MachO/Options.td b/lld/MachO/Options.td
index 9001e85582c12..abc7697fde873 100644
--- a/lld/MachO/Options.td
+++ b/lld/MachO/Options.td
@@ -875,7 +875,6 @@ def setuid_safe : Flag<["-"], "setuid_safe">,
     Group<grp_rare>;
 def interposable : Flag<["-"], "interposable">,
     HelpText<"Indirects access to all to exported symbols in a dylib">,
-    Flags<[HelpHidden]>,
     Group<grp_rare>;
 def multi_module : Flag<["-"], "multi_module">,
     Alias<interposable>,
diff --git a/lld/MachO/SymbolTable.cpp b/lld/MachO/SymbolTable.cpp
index a61e60a944fb4..f6ac2019533ca 100644
--- a/lld/MachO/SymbolTable.cpp
+++ b/lld/MachO/SymbolTable.cpp
@@ -204,8 +204,9 @@ Defined *SymbolTable::addDefined(StringRef name, InputFile *file,
 
   // With -flat_namespace, all extern symbols in dylibs are interposable.
   // FIXME: Add support for `-interposable` (PR53680).
-  bool interposable = config->namespaceKind == NamespaceKind::flat &&
-                      config->outputType != MachO::MH_EXECUTE &&
+  bool interposable = ((config->namespaceKind == NamespaceKind::flat &&
+                        config->outputType != MachO::MH_EXECUTE) ||
+                       config->interposable) &&
                       !isPrivateExtern;
   Defined *defined = replaceSymbol<Defined>(
       s, name, file, isec, value, size, isWeakDef, /*isExternal=*/true,

Copy link

⚠️ We detected that you are using a GitHub private e-mail address to contribute to the repo.
Please turn off Keep my email addresses private setting in your account.
See LLVM Discourse for more information.

@carlocab carlocab requested review from nico, int3, ellishg and alx32 March 18, 2025 15:28
Copy link
Member

@carlocab carlocab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Could you add tests for this?

@alx32 alx32 requested a review from smeenai March 18, 2025 15:53
@johnno1962
Copy link
Contributor Author

Looks like I've wrangled the git/CI problems now. Anything else you'd like let me know. Rest assured I've been testing this version of the linker in the field on a reasonably sized project and it and the new -interposable functionality are working well.

@johnno1962 johnno1962 requested review from carlocab and MaskRay March 21, 2025 13:36
@johnno1962
Copy link
Contributor Author

HI Folks, how about landing this PR before it collects any conflicts? It's a very limited change to unlock a latent capability of your code and as such possess very limited risk of causing a collateral regression. Interposing is an very useful thing for a linker to be able to do which I use in the project InjectionIII which is motivated here. I'm interested in ldd as it has started turning up in Apple's daily build of their developer toolchain so it looks like they are evaluating it. It would be a shame if it were to not include the -interposable option given how easily it could.

@smeenai
Copy link
Collaborator

smeenai commented Mar 26, 2025

Could you verify that Apple's linker has the same behavior around:

  • -interposable affecting executables as well as dylibs?
  • -interposable affecting all definitions including _main?

If so, this looks good to me.

@johnno1962
Copy link
Contributor Author

Hi @smeenai, I can confirm your first question by observation of the linker in use in a large-ish project that requires interposing for dynamic updates. By chance Apple's Xcode allows you to choose an executable or dynamic library for your "main" image containing all code and both cases definitely work with "my" ldd and Apple's linker so indirecting symbols in an executable is more faithful. This is the reason the code I'm proposing is different from your original suggestion last week.

Your second question about _main I can't confirm as I can't use llvm-objdump to see the symbols being indirected using Apple's linker in fact it doesn't see any indirections for all my test executables even using ldd even though they are definitely interposing according to how the application changes behaviour after using fishhook. Can you think of an explanation for this?

@johnno1962
Copy link
Contributor Author

I'm sensing a reluctance to take my word for Apple's linker applying -interposable for symbols inside executables. To close this out how about I go with @smeenai's original suggestion in the mentioned issue as below, update the test and not implement this. With Xcode 16 an app's "main image" is a .dylib now anyway so it's not important to me to fight this battle.

  bool interposable = (config->namespaceKind == NamespaceKind::flat || config->interposable) &&
                      config->outputType != MachO::MH_EXECUTE &&
                      !isPrivateExtern;

@smeenai
Copy link
Collaborator

smeenai commented Mar 27, 2025

Sorry, not doubting your word at all, just got busy yesterday :) Give me a bit to look at this a bit more in depth.

@smeenai
Copy link
Collaborator

smeenai commented Mar 27, 2025

If you're checking for lazy symbol bindings, targeting newer OS versions uses chained fixups by default, which gets rid of lazy bindings. The difference between interposable and non-interposable calls is whether they go through a stub, i.e.

$ cat interposable_test.c
void foo() {}
void bar() { foo(); }

$ clang -c interposable_test.c
$ ld -dylib -o normal.dylib interposable_test.o
$ ld -dylib -interposable -o interposable.dylib interposable_test.o

$ objdump -d normal.dylib

normal.dylib:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000000003f88 <_foo>:
    3f88: d65f03c0     	ret

0000000000003f8c <_bar>:
    3f8c: a9bf7bfd     	stp	x29, x30, [sp, #-0x10]!
    3f90: 910003fd     	mov	x29, sp
    3f94: 97fffffd     	bl	0x3f88 <_foo>
    3f98: a8c17bfd     	ldp	x29, x30, [sp], #0x10
    3f9c: d65f03c0     	ret

$ objdump -d interposable.dylib

interposable.dylib:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000000003f7c <_foo>:
    3f7c: d65f03c0     	ret

0000000000003f80 <_bar>:
    3f80: a9bf7bfd     	stp	x29, x30, [sp, #-0x10]!
    3f84: 910003fd     	mov	x29, sp
    3f88: 94000003     	bl	0x3f94
    3f8c: a8c17bfd     	ldp	x29, x30, [sp], #0x10
    3f90: d65f03c0     	ret

Disassembly of section __TEXT,__stubs:

0000000000003f94 <__stubs>:
    3f94: b0000010     	adrp	x16, 0x4000
    3f98: f9400210     	ldr	x16, [x16]
    3f9c: d61f0200     	br	x16

The stub is what enables the interposing to work, and the binding for the stub can be lazy or non-lazy.

You can repeat the same experiment with an executable and confirm that -interposable applies to it, as you said :) Could you just change the test to check that the call is going through a stub instead of checking for a lazy binding? Then this looks good to me.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Mar 27, 2025

Thanks for the info. Modifying the test to use Apple's linker so:

# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/2.s -o %t/2.o
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/3.s -o %t/3.o
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/main.s -o %t/main.o
# RUN:~/D2/Xcode162.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch x86_64 -isysroot ~/D2/Xcode162.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Xlinker -interposable -lSystem -o %t/main2 %t/main.o %t/2.o %t/3.o

#--- 2.s
# my_lib: This contains the exported function
.globl my_func
my_func:
  retq

#--- 3.s
# my_user.s: This is the user/caller of the
#            exported function
.text
my_user:
  callq my_func()
  retq

#--- main.s
# main.s: dummy exec/main loads the exported function.
# This is basically a way to say `my_user` should get
# `my_func` from this executable.
.globl _main
.text
 _main:
  retq

Yields:

Mac-minii:llvm-project$ objdump -d ./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2 

./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000100000f9a <_main>:
100000f9a: c3                          	retq

0000000100000f9b <my_func>:
100000f9b: c3                          	retq

0000000100000f9c <my_user>:
100000f9c: e8 01 00 00 00              	callq	0x100000fa2
100000fa1: c3                          	retq

Disassembly of section __TEXT,__stubs:

0000000100000fa2 <__stubs>:
100000fa2: ff 25 58 00 00 00           	jmpq	*0x58(%rip)             ## 0x100001000

So it is setting up a stub for a symbol in an executable. Does that answer your first question?

Mac-minii:llvm-project$ file !$
file ./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2
./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2: Mach-O 64-bit executable x86_64

Without the -interposable flag it looks like this:

Mac-minii:llvm-project$ objdump -d ./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2 

./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000100000fa0 <_main>:
100000fa0: c3                          	retq

0000000100000fa1 <my_func>:
100000fa1: c3                          	retq

0000000100000fa2 <my_user>:
100000fa2: e8 fa ff ff ff              	callq	0x100000fa1 <my_func>
100000fa7: c3                          	retq

@smeenai
Copy link
Collaborator

smeenai commented Mar 27, 2025

You can repeat the same experiment with an executable and confirm that -interposable applies to it, as you said :)

Ah sorry, by this I meant that you were correct in what you'd said, not that I was asking you to run that experiment to verify it :) Either way though, the behavior change here looks good; we should just update the test to verify the stub instead of the lazy bind.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Mar 27, 2025

Modifying the linking to force the platform version:

# RUN: ~/D2/Xcode162.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -fuse-ld=~/Developer/llvm-project/build/bin/ld64.lld -arch x86_64 -isysroot ~/D2/Xcode162.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Xlinker -interposable -lSystem -o %t/main2 %t/main.o %t/2.o %t/3.o

Yields:

Mac-minii:llvm-project$ objdump -d ./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2 

./build/tools/lld/test/MachO/Output/interposable.s.tmp/main2:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

00000001000003a0 <_main>:
1000003a0: c3                          	retq

00000001000003a1 <my_func>:
1000003a1: c3                          	retq

00000001000003a2 <my_user>:
1000003a2: e8 07 00 00 00              	callq	0x1000003ae <dyld_stub_binder+0x1000003ae>
1000003a7: c3                          	retq

Disassembly of section __TEXT,__stubs:

00000001000003a8 <__stubs>:
1000003a8: ff 25 52 0c 00 00           	jmpq	*0xc52(%rip)            ## 0x100001000 <dyld_stub_binder+0x100001000>
1000003ae: ff 25 54 0c 00 00           	jmpq	*0xc54(%rip)            ## 0x100001008 <dyld_stub_binder+0x100001008>

But how to force the platform version for lld directly in an llvm specific test?

Mac-minii:llvm-project$ ./build/bin/ld64.lld -arch x86_64 -lSystem -platform_version 15
ld64.lld: error: -platform_version: missing argument
ld64.lld: error: must specify -platform_version
ld64.lld: error: missing or unsupported -arch x86_64

I'm thinking the existing test proves the option is working.

@smeenai
Copy link
Collaborator

smeenai commented Mar 27, 2025

You can add -v to your clang command to see how it invokes lld, to check what arguments to pass for the platform version. You don't need to necessarily use the newer platform version in your test though, just change it to do objdump -d instead and verify that the call to my_func goes through a stub.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Mar 27, 2025

How about this test (before I commit it):

# REQUIRES: x86

# RUN: rm -rf %t; split-file %s %t
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/2.s -o %t/2.o
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/3.s -o %t/3.o
# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/main.s -o %t/main.o

# RUN: %lld -arch x86_64 -interposable -lSystem -o %t/main3 %t/main.o %t/2.o %t/3.o
# RUN: llvm-objdump -d %t/main3 | FileCheck %s --check-prefix BUNDLE-OBJ
BUNDLE-OBJ: [[#%x,]] <my_user>:
BUNDLE-OBJ: [[#%x,]]: [[#%x,]] [[#%x,]] [[#%x,]] [[#%x,]] [[#%x,]] callq 0x[[#%x,]] <dyld_stub_binder+0x[[#%x,]]>
BUNDLE-OBJ: [[#%x,]]: [[#%x,]] retq

#--- 2.s
# my_lib: This contains the exported function
.globl my_func
my_func:
  retq

#--- 3.s
# my_user.s: This is the user/caller of the
#            exported function
.text
my_user:
  callq my_func()
  retq

#--- main.s
# main.s: dummy exec/main loads the exported function.
# This is basically a way to say `my_user` should get
# `my_func` from this executable.
.globl _main
.text
 _main:
  retq

objdump -d output is:

Mac-minii:llvm-project$ objdump -d ./build/tools/lld/test/MachO/Output/interposable.s.tmp/main3

./build/tools/lld/test/MachO/Output/interposable.s.tmp/main3:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

00000001000004e8 <_main>:
1000004e8: c3                          	retq

00000001000004e9 <my_func>:
1000004e9: c3                          	retq

00000001000004ea <my_user>:
1000004ea: e8 07 00 00 00              	callq	0x1000004f6 <dyld_stub_binder+0x1000004f6>
1000004ef: c3                          	retq

Disassembly of section __TEXT,__stubs:

00000001000004f0 <__stubs>:
1000004f0: ff 25 0a 1b 00 00           	jmpq	*0x1b0a(%rip)           ## 0x100002000 <dyld_stub_binder+0x100002000>
1000004f6: ff 25 0c 1b 00 00           	jmpq	*0x1b0c(%rip)           ## 0x100002008 <dyld_stub_binder+0x100002008>

Disassembly of section __TEXT,__stub_helper:

00000001000004fc <__stub_helper>:
1000004fc: 4c 8d 1d 0d 1b 00 00        	leaq	0x1b0d(%rip), %r11      ## 0x100002010 <__dyld_private>
100000503: 41 53                       	pushq	%r11
100000505: ff 25 f5 0a 00 00           	jmpq	*0xaf5(%rip)            ## 0x100001000 <dyld_stub_binder+0x100001000>
10000050b: 90                          	nop
10000050c: 68 00 00 00 00              	pushq	$0x0
100000511: e9 e6 ff ff ff              	jmp	0x1000004fc <__stub_helper>
100000516: 68 0c 00 00 00              	pushq	$0xc
10000051b: e9 dc ff ff ff              	jmp	0x1000004fc <__stub_helper>

@smeenai
Copy link
Collaborator

smeenai commented Mar 27, 2025

llvm-objdump will always be available, so we can use that. For the test, I found that using --macho alongside -d gets it to print the stub information directly, which we can take advantage of:

BUNDLE-OBJ-LABEL: my_user:
BUNDLE-OBJ-NEXT:          callq   [[#%#x,]] ## symbol stub for: my_func

That's using a numeric substitution block to capture the hex value, and it's also taking advantage of FileCheck doing partial matches on lines by default (so you don't need to specify the entire contents of a line to match against, just the bits you care about).

I'd also suggest renaming the functions to _my_user and _my_friend, since the leading underscore is user label prefix on Mach-O.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Mar 28, 2025

Thanks for all your patience @smeenai, I think the test is updated and the PR squashed now. Anything else you can think of?

Copy link
Collaborator

@smeenai smeenai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks for the patience with the reviews!

@smeenai
Copy link
Collaborator

smeenai commented Mar 28, 2025

I'll land this for you when all tests have passed.

@smeenai smeenai merged commit da84a7d into llvm:main Mar 28, 2025
9 of 11 checks passed
Copy link

@johnno1962 Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

@johnno1962
Copy link
Contributor Author

Thanks @smeenai, Two weeks turnaround for my first llvm PR - Not bad all told. Perhaps one day I'll pursue my theory that all object files should be stored in zip format for faster disk I/O but that'll keep for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants