From 395963777f56581db5436a895f492c44a456b4db Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Tue, 11 Jun 2024 02:12:42 +0100 Subject: [PATCH] macho/load_commands: support new macOS 15 dylib use command (#625) --- .gitignore | 3 + lib/macho/load_commands.rb | 101 +++++++++++++++++- .../x86_64/dylib_use_command-weak-delay.bin | Bin 0 -> 8472 bytes test/src/Makefile | 27 +++-- test/test_create_load_commands.rb | 22 +++- test/test_macho.rb | 41 +++++-- test/test_serialize_load_commands.rb | 19 ++++ 7 files changed, 197 insertions(+), 16 deletions(-) create mode 100755 test/bin/x86_64/dylib_use_command-weak-delay.bin diff --git a/.gitignore b/.gitignore index b412c7e4e..d332b2de0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ .ruby-version .idea/ .vscode/ + +# macOS metadata file +.DS_Store diff --git a/lib/macho/load_commands.rb b/lib/macho/load_commands.rb index 183f6a6a2..1ed24a58b 100644 --- a/lib/macho/load_commands.rb +++ b/lib/macho/load_commands.rb @@ -111,7 +111,7 @@ module LoadCommands # "reserved for internal use only", no public struct :LC_PREPAGE => "LoadCommand", :LC_DYSYMTAB => "DysymtabCommand", - :LC_LOAD_DYLIB => "DylibCommand", + :LC_LOAD_DYLIB => "DylibUseCommand", :LC_ID_DYLIB => "DylibCommand", :LC_LOAD_DYLINKER => "DylinkerCommand", :LC_ID_DYLINKER => "DylinkerCommand", @@ -123,7 +123,7 @@ module LoadCommands :LC_SUB_LIBRARY => "SubLibraryCommand", :LC_TWOLEVEL_HINTS => "TwolevelHintsCommand", :LC_PREBIND_CKSUM => "PrebindCksumCommand", - :LC_LOAD_WEAK_DYLIB => "DylibCommand", + :LC_LOAD_WEAK_DYLIB => "DylibUseCommand", :LC_SEGMENT_64 => "SegmentCommand64", :LC_ROUTINES_64 => "RoutinesCommand64", :LC_UUID => "UUIDCommand", @@ -195,6 +195,20 @@ module LoadCommands :SG_READ_ONLY => 0x10, }.freeze + # association of dylib use flag symbols to values + # @api private + DYLIB_USE_FLAGS = { + :DYLIB_USE_WEAK_LINK => 0x1, + :DYLIB_USE_REEXPORT => 0x2, + :DYLIB_USE_UPWARD => 0x4, + :DYLIB_USE_DELAYED_INIT => 0x8, + }.freeze + + # the marker used to denote a newer style dylib use command. + # the value is the timestamp 24 January 1984 18:12:16 + # @api private + DYLIB_USE_MARKER = 0x1a741800 + # The top-level Mach-O load command structure. # # This is the most generic load command -- only the type ID and size are @@ -233,6 +247,13 @@ def self.create(cmd_sym, *args) # cmd will be filled in, view and cmdsize will be left unpopulated klass_arity = klass.min_args - 3 + # macOS 15 introduces a new dylib load command that adds a flags field to the end. + # It uses the same commands with it dynamically being created if the dylib has a flags field + if klass == DylibUseCommand && (args[1] != DYLIB_USE_MARKER || args.size <= DylibCommand.min_args - 3) + klass = DylibCommand + klass_arity = klass.min_args - 3 + end + raise LoadCommandCreationArityError.new(cmd_sym, klass_arity, args.size) if klass_arity > args.size klass.new(nil, cmd, nil, *args) @@ -528,6 +549,23 @@ class DylibCommand < LoadCommand # @return [Integer] the library's compatibility version number field :compatibility_version, :uint32 + # @example + # puts "this dylib is weakly loaded" if dylib_command.flag?(:DYLIB_USE_WEAK_LINK) + # @param flag [Symbol] a dylib use command flag symbol + # @return [Boolean] true if `flag` applies to this dylib command + def flag?(flag) + case cmd + when LOAD_COMMAND_CONSTANTS[:LC_LOAD_WEAK_DYLIB] + flag == :DYLIB_USE_WEAK_LINK + when LOAD_COMMAND_CONSTANTS[:LC_REEXPORT_DYLIB] + flag == :DYLIB_USE_REEXPORT + when LOAD_COMMAND_CONSTANTS[:LC_LOAD_UPWARD_DYLIB] + flag == :DYLIB_USE_UPWARD + else + false + end + end + # @param context [SerializationContext] # the context # @return [String] the serialized fields of the load command @@ -553,6 +591,65 @@ def to_h end end + # The newer format of load command representing some aspect of shared libraries, + # depending on filetype. Corresponds to LC_LOAD_DYLIB or LC_LOAD_WEAK_DYLIB. + class DylibUseCommand < DylibCommand + # @return [Integer] any flags associated with this dylib use command + field :flags, :uint32 + + alias marker timestamp + + # Instantiates a new DylibCommand or DylibUseCommand. + # macOS 15 and later use a new format for dylib commands (DylibUseCommand), + # which is determined based on a special timestamp and the name offset. + # @param view [MachO::MachOView] the load command's raw view + # @return [DylibCommand] the new dylib load command + # @api private + def self.new_from_bin(view) + dylib_command = DylibCommand.new_from_bin(view) + + if dylib_command.timestamp == DYLIB_USE_MARKER && + dylib_command.name.to_i == DylibUseCommand.bytesize + super(view) + else + dylib_command + end + end + + # @example + # puts "this dylib is weakly loaded" if dylib_command.flag?(:DYLIB_USE_WEAK_LINK) + # @param flag [Symbol] a dylib use command flag symbol + # @return [Boolean] true if `flag` applies to this dylib command + def flag?(flag) + flag = DYLIB_USE_FLAGS[flag] + + return false if flag.nil? + + flags & flag == flag + end + + # @param context [SerializationContext] + # the context + # @return [String] the serialized fields of the load command + # @api private + def serialize(context) + format = Utils.specialize_format(self.class.format, context.endianness) + string_payload, string_offsets = Utils.pack_strings(self.class.bytesize, + context.alignment, + :name => name.to_s) + cmdsize = self.class.bytesize + string_payload.bytesize + [cmd, cmdsize, string_offsets[:name], marker, current_version, + compatibility_version, flags].pack(format) + string_payload + end + + # @return [Hash] a hash representation of this {DylibUseCommand} + def to_h + { + "flags" => flags, + }.merge super + end + end + # A load command representing some aspect of the dynamic linker, depending # on filetype. Corresponds to LC_ID_DYLINKER, LC_LOAD_DYLINKER, and # LC_DYLD_ENVIRONMENT. diff --git a/test/bin/x86_64/dylib_use_command-weak-delay.bin b/test/bin/x86_64/dylib_use_command-weak-delay.bin new file mode 100755 index 0000000000000000000000000000000000000000..5fc52a9242ea68bfee674c980b175095661147cc GIT binary patch literal 8472 zcmeHM&ubGw6n@(u+G;dWQ0fn41r-%(t%`UNT5af7r7dm6QjxJ~woM>SO0vOL5duZ9 zpdLKcTMr&QcPOl8y6TFoW-38BUz{@m&Gnv}dT6u|M}=z;6{V`hd--Bk<%_c=bTrp{Y3fayB;w{8(Ry5m zRb6MGf1+Pq9T^_4uLk&ztwXpTrse*iGKMat=1P%e0N%P@2V%7#iTRE8(*g+_(3 zp+@~td)?id@7)MOmn5J))k?Xikk9mFmkL>6${{GnG39vjDseIMZ1~072V>V>pM%eZ zI#3aAj-<9V|LwQiPQ#<;$8N|;%#U@r&!Ylg?GhY^>Ff#)CR$k0kxo1}$AdB5@9yo! zu<{wkK{#IKu_os(GKim_6U!N2ss#Rg_mybk2}E*StF8#w{FK3Kz*Z~+mI2FvWxz6E z8L$jk1}p>r3j;U3mCxStm(<7fPfmH|;dgJP`ptWKc^}Xl+2C2Z_no)=A$8~d@8d-v z&o2~8XWUz*av^(k2ORqRug~$Yqb<+pJoopZrSjX9(x;z|2frN>sFKo^w7>p&(FmzE zOlw&NECZGS%YbFTGGH073|Iy%1C{~HfMvik@GmmpVp6>OoP^eI31JwC-ktMKFcfX4 zaElwcjAU*L|CFGp1>AxMvvq){qhv1u`&(uG2rM<9Q~shqQw@BT^QW_ZSvu#Z^F`Su yIv@;BI`x;Z{(fi@2LJaU=KmpR!?((@CamvG;d60A{rBsXex1q|N(+85Hsn9uKbbNB literal 0 HcmV?d00001 diff --git a/test/src/Makefile b/test/src/Makefile index 798ec6075..ed3db27c7 100644 --- a/test/src/Makefile +++ b/test/src/Makefile @@ -1,6 +1,7 @@ # Usage: # make USE=10.6-xcode3.2.6 # make USE=10.11-xcode7.3 +# make USE=15-xcode16.0 HELLO_SRC = hello.c LIBHELLO_SRC = libhello.c @@ -42,13 +43,24 @@ else ifeq ($(USE),10.6-xcode3.2.6) USE_DIRS := i386 x86_64 ppc fat-i386-x86_64 fat-i386-ppc NO_UPWARD := 1 NO_LAZY := 1 + NO_DELAY_INIT := 1 else ifeq ($(USE),10.11-xcode7.3) USE_DIRS := i386 x86_64 fat-i386-x86_64 + NO_DELAY_INIT := 1 +else ifeq ($(USE),15-xcode16.0) + USE_DIRS := x86_64 + NO_LAZY := 1 else # Warn about unspecified subset, but effectively fall back to 10.11-xcode7.3. $(warning USE - Option either unset or invalid. Using a safe fallback.) - $(warning USE - Valid choices: all, 10.6-xcode3.2.6, 10.11-xcode7.3.) + $(warning USE - Valid choices: all, 10.6-xcode3.2.6, 10.11-xcode7.3, 15-xcode16.0.) USE_DIRS := i386 x86_64 fat-i386-x86_64 + NO_DELAY_INIT := 1 + NO_LAZY := 1 +endif + +ifeq ($(NO_DELAY_INIT),) + TARGET_FILES += dylib_use_command-weak-delay.bin endif # Setup target names from all/used architecture directories. @@ -84,10 +96,10 @@ $(ALL_DIRS): # Setup architecture-specific per-file targets (`/`). %/hello.o: $(HELLO_SRC) % - $(CC) $(ARCH_FLAGS) -o $@ -c $< + $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -c $< %/hello.bin: $(HELLO_SRC) % - $(CC) $(ARCH_FLAGS) -o $@ $(RPATH_FLAGS) $< + $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ $(RPATH_FLAGS) $< %/hello_expected.bin: %/hello.bin cp $< $@ @@ -97,18 +109,21 @@ $(ALL_DIRS): cp $< $@ install_name_tool -rpath made_up_path /usr/lib $@ +%/dylib_use_command-weak-delay.bin: $(HELLO_SRC) % + $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -Wl,-weak-l,z -Wl,-delay-l,z $< + %/libhello.dylib: $(LIBHELLO_SRC) % - $(CC) $(ARCH_FLAGS) -o $@ -dynamiclib $< + $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $< %/libhello_expected.dylib: %/libhello.dylib cp $< $@ install_name_tool -id test $@ %/libextrahello.dylib: $(LIBHELLO_SRC) % %/libhello.dylib - $(CC) $(ARCH_FLAGS) -o $@ -dynamiclib $< $(LIBEXTRA_LDADD) + $(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $< $(LIBEXTRA_LDADD) %/hellobundle.so: $(LIBHELLO_SRC) % - $(CC) $(ARCH_FLAGS) -bundle $< -o $@ + $(CC) $(CFLAGS) $(ARCH_FLAGS) -bundle $< -o $@ # build inconsistent binaries .PHONY: inconsistent diff --git a/test/test_create_load_commands.rb b/test/test_create_load_commands.rb index 213342b54..73cd75fb0 100644 --- a/test/test_create_load_commands.rb +++ b/test/test_create_load_commands.rb @@ -24,7 +24,7 @@ def test_create_dylib_commands lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", 0, 0, 0) assert lc - assert_kind_of MachO::LoadCommands::DylibCommand, lc + assert_instance_of MachO::LoadCommands::DylibCommand, lc assert lc.name assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name assert_equal "test", lc.name.to_s @@ -36,6 +36,26 @@ def test_create_dylib_commands end end + def test_create_dylib_commands_new + # all dylib commands are creatable, so test them all + dylib_commands = %i[LC_LOAD_DYLIB LC_LOAD_WEAK_DYLIB] + dylib_commands.each do |cmd_sym| + lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", MachO::LoadCommands::DYLIB_USE_MARKER, 0, 0, 0) + + assert lc + assert_instance_of MachO::LoadCommands::DylibUseCommand, lc + assert lc.name + assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name + assert_equal "test", lc.name.to_s + assert_equal lc.name.to_s, lc.to_s + assert_equal MachO::LoadCommands::DYLIB_USE_MARKER, lc.timestamp + assert_equal 0, lc.current_version + assert_equal 0, lc.compatibility_version + assert_equal 0, lc.flags + assert_instance_of String, lc.view.inspect + end + end + def test_create_rpath_command lc = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, "test") diff --git a/test/test_macho.rb b/test/test_macho.rb index 53d720a14..5e51cbff7 100644 --- a/test/test_macho.rb +++ b/test/test_macho.rb @@ -235,12 +235,12 @@ def test_dylib def test_extra_dylib filenames = SINGLE_ARCHES.map { |a| fixture(a, "libextrahello.dylib") } - unusual_dylib_lcs = %i[ - LC_LOAD_UPWARD_DYLIB - LC_LAZY_LOAD_DYLIB - LC_LOAD_WEAK_DYLIB - LC_REEXPORT_DYLIB - ] + unusual_dylib_lcs = { + LC_LOAD_UPWARD_DYLIB: :DYLIB_USE_UPWARD, + LC_LAZY_LOAD_DYLIB: nil, + LC_LOAD_WEAK_DYLIB: :DYLIB_USE_WEAK_LINK, + LC_REEXPORT_DYLIB: :DYLIB_USE_REEXPORT, + } filenames.each do |fn| file = MachO::MachOFile.new(fn) @@ -248,7 +248,7 @@ def test_extra_dylib assert file.dylib? # make sure we can read more unusual dylib load commands - unusual_dylib_lcs.each do |cmdname| + unusual_dylib_lcs.each do |cmdname, flag_name| lc = file[cmdname].first # PPC and x86-family binaries don't have the same dylib LCs, so ignore @@ -262,10 +262,37 @@ def test_extra_dylib assert dylib_name assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, dylib_name + + assert lc.flag?(flag_name) if flag_name + (unusual_dylib_lcs.values - [flag_name]).compact.each do |other_flag_name| + refute lc.flag?(other_flag_name) + end end end end + def test_dylib_use_command + filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") } + + filenames.each do |fn| + file = MachO::MachOFile.new(fn) + + lc = file[:LC_LOAD_WEAK_DYLIB].first + lc2 = file[:LC_LOAD_DYLIB].first + + assert_instance_of MachO::LoadCommands::DylibUseCommand, lc + assert_instance_of MachO::LoadCommands::DylibCommand, lc2 + + refute_equal lc.flags, 0 + + assert lc.flag?(:DYLIB_USE_WEAK_LINK) + assert lc.flag?(:DYLIB_USE_DELAYED_INIT) + refute lc.flag?(:DYLIB_USE_UPWARD) + + refute lc2.flag?(:DYLIB_USE_WEAK_LINK) + end + end + def test_bundle filenames = SINGLE_ARCHES.map { |a| fixture(a, "hellobundle.so") } diff --git a/test/test_serialize_load_commands.rb b/test/test_serialize_load_commands.rb index 613331da1..49fba401a 100644 --- a/test/test_serialize_load_commands.rb +++ b/test/test_serialize_load_commands.rb @@ -69,6 +69,25 @@ def test_serialize_load_dylib lc.compatibility_version) blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] + assert_instance_of lc.class, lc2 + assert_equal blob, lc.serialize(ctx) + assert_equal blob, lc2.serialize(ctx) + end + end + + def test_serialize_load_dylib_new + filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") } + + filenames.each do |filename| + file = MachO::MachOFile.new(filename) + ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file) + lc = file[:LC_LOAD_WEAK_DYLIB].first + lc2 = MachO::LoadCommands::LoadCommand.create(:LC_LOAD_WEAK_DYLIB, lc.name.to_s, + lc.marker, lc.current_version, + lc.compatibility_version, lc.flags) + blob = lc.view.raw_data[lc.view.offset, lc.cmdsize] + + assert_instance_of lc.class, lc2 assert_equal blob, lc.serialize(ctx) assert_equal blob, lc2.serialize(ctx) end