Skip to content

Commit

Permalink
macho/load_commands: support new macOS 15 dylib use command (#625)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bo98 authored Jun 11, 2024
1 parent a3fc5a5 commit 3959637
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
.ruby-version
.idea/
.vscode/

# macOS metadata file
.DS_Store
101 changes: 99 additions & 2 deletions lib/macho/load_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
Binary file added test/bin/x86_64/dylib_use_command-weak-delay.bin
Binary file not shown.
27 changes: 21 additions & 6 deletions test/src/Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -84,10 +96,10 @@ $(ALL_DIRS):

# Setup architecture-specific per-file targets (`<arch>/<file>`).
%/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 $< $@
Expand All @@ -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
Expand Down
22 changes: 21 additions & 1 deletion test/test_create_load_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")

Expand Down
41 changes: 34 additions & 7 deletions test/test_macho.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,20 @@ 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)

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
Expand All @@ -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") }

Expand Down
19 changes: 19 additions & 0 deletions test/test_serialize_load_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3959637

Please sign in to comment.