Skip to content

Commit 3959637

Browse files
authored
macho/load_commands: support new macOS 15 dylib use command (#625)
1 parent a3fc5a5 commit 3959637

File tree

7 files changed

+197
-16
lines changed

7 files changed

+197
-16
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@
2222
.ruby-version
2323
.idea/
2424
.vscode/
25+
26+
# macOS metadata file
27+
.DS_Store

lib/macho/load_commands.rb

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ module LoadCommands
111111
# "reserved for internal use only", no public struct
112112
:LC_PREPAGE => "LoadCommand",
113113
:LC_DYSYMTAB => "DysymtabCommand",
114-
:LC_LOAD_DYLIB => "DylibCommand",
114+
:LC_LOAD_DYLIB => "DylibUseCommand",
115115
:LC_ID_DYLIB => "DylibCommand",
116116
:LC_LOAD_DYLINKER => "DylinkerCommand",
117117
:LC_ID_DYLINKER => "DylinkerCommand",
@@ -123,7 +123,7 @@ module LoadCommands
123123
:LC_SUB_LIBRARY => "SubLibraryCommand",
124124
:LC_TWOLEVEL_HINTS => "TwolevelHintsCommand",
125125
:LC_PREBIND_CKSUM => "PrebindCksumCommand",
126-
:LC_LOAD_WEAK_DYLIB => "DylibCommand",
126+
:LC_LOAD_WEAK_DYLIB => "DylibUseCommand",
127127
:LC_SEGMENT_64 => "SegmentCommand64",
128128
:LC_ROUTINES_64 => "RoutinesCommand64",
129129
:LC_UUID => "UUIDCommand",
@@ -195,6 +195,20 @@ module LoadCommands
195195
:SG_READ_ONLY => 0x10,
196196
}.freeze
197197

198+
# association of dylib use flag symbols to values
199+
# @api private
200+
DYLIB_USE_FLAGS = {
201+
:DYLIB_USE_WEAK_LINK => 0x1,
202+
:DYLIB_USE_REEXPORT => 0x2,
203+
:DYLIB_USE_UPWARD => 0x4,
204+
:DYLIB_USE_DELAYED_INIT => 0x8,
205+
}.freeze
206+
207+
# the marker used to denote a newer style dylib use command.
208+
# the value is the timestamp 24 January 1984 18:12:16
209+
# @api private
210+
DYLIB_USE_MARKER = 0x1a741800
211+
198212
# The top-level Mach-O load command structure.
199213
#
200214
# This is the most generic load command -- only the type ID and size are
@@ -233,6 +247,13 @@ def self.create(cmd_sym, *args)
233247
# cmd will be filled in, view and cmdsize will be left unpopulated
234248
klass_arity = klass.min_args - 3
235249

250+
# macOS 15 introduces a new dylib load command that adds a flags field to the end.
251+
# It uses the same commands with it dynamically being created if the dylib has a flags field
252+
if klass == DylibUseCommand && (args[1] != DYLIB_USE_MARKER || args.size <= DylibCommand.min_args - 3)
253+
klass = DylibCommand
254+
klass_arity = klass.min_args - 3
255+
end
256+
236257
raise LoadCommandCreationArityError.new(cmd_sym, klass_arity, args.size) if klass_arity > args.size
237258

238259
klass.new(nil, cmd, nil, *args)
@@ -528,6 +549,23 @@ class DylibCommand < LoadCommand
528549
# @return [Integer] the library's compatibility version number
529550
field :compatibility_version, :uint32
530551

552+
# @example
553+
# puts "this dylib is weakly loaded" if dylib_command.flag?(:DYLIB_USE_WEAK_LINK)
554+
# @param flag [Symbol] a dylib use command flag symbol
555+
# @return [Boolean] true if `flag` applies to this dylib command
556+
def flag?(flag)
557+
case cmd
558+
when LOAD_COMMAND_CONSTANTS[:LC_LOAD_WEAK_DYLIB]
559+
flag == :DYLIB_USE_WEAK_LINK
560+
when LOAD_COMMAND_CONSTANTS[:LC_REEXPORT_DYLIB]
561+
flag == :DYLIB_USE_REEXPORT
562+
when LOAD_COMMAND_CONSTANTS[:LC_LOAD_UPWARD_DYLIB]
563+
flag == :DYLIB_USE_UPWARD
564+
else
565+
false
566+
end
567+
end
568+
531569
# @param context [SerializationContext]
532570
# the context
533571
# @return [String] the serialized fields of the load command
@@ -553,6 +591,65 @@ def to_h
553591
end
554592
end
555593

594+
# The newer format of load command representing some aspect of shared libraries,
595+
# depending on filetype. Corresponds to LC_LOAD_DYLIB or LC_LOAD_WEAK_DYLIB.
596+
class DylibUseCommand < DylibCommand
597+
# @return [Integer] any flags associated with this dylib use command
598+
field :flags, :uint32
599+
600+
alias marker timestamp
601+
602+
# Instantiates a new DylibCommand or DylibUseCommand.
603+
# macOS 15 and later use a new format for dylib commands (DylibUseCommand),
604+
# which is determined based on a special timestamp and the name offset.
605+
# @param view [MachO::MachOView] the load command's raw view
606+
# @return [DylibCommand] the new dylib load command
607+
# @api private
608+
def self.new_from_bin(view)
609+
dylib_command = DylibCommand.new_from_bin(view)
610+
611+
if dylib_command.timestamp == DYLIB_USE_MARKER &&
612+
dylib_command.name.to_i == DylibUseCommand.bytesize
613+
super(view)
614+
else
615+
dylib_command
616+
end
617+
end
618+
619+
# @example
620+
# puts "this dylib is weakly loaded" if dylib_command.flag?(:DYLIB_USE_WEAK_LINK)
621+
# @param flag [Symbol] a dylib use command flag symbol
622+
# @return [Boolean] true if `flag` applies to this dylib command
623+
def flag?(flag)
624+
flag = DYLIB_USE_FLAGS[flag]
625+
626+
return false if flag.nil?
627+
628+
flags & flag == flag
629+
end
630+
631+
# @param context [SerializationContext]
632+
# the context
633+
# @return [String] the serialized fields of the load command
634+
# @api private
635+
def serialize(context)
636+
format = Utils.specialize_format(self.class.format, context.endianness)
637+
string_payload, string_offsets = Utils.pack_strings(self.class.bytesize,
638+
context.alignment,
639+
:name => name.to_s)
640+
cmdsize = self.class.bytesize + string_payload.bytesize
641+
[cmd, cmdsize, string_offsets[:name], marker, current_version,
642+
compatibility_version, flags].pack(format) + string_payload
643+
end
644+
645+
# @return [Hash] a hash representation of this {DylibUseCommand}
646+
def to_h
647+
{
648+
"flags" => flags,
649+
}.merge super
650+
end
651+
end
652+
556653
# A load command representing some aspect of the dynamic linker, depending
557654
# on filetype. Corresponds to LC_ID_DYLINKER, LC_LOAD_DYLINKER, and
558655
# LC_DYLD_ENVIRONMENT.
8.27 KB
Binary file not shown.

test/src/Makefile

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Usage:
22
# make USE=10.6-xcode3.2.6
33
# make USE=10.11-xcode7.3
4+
# make USE=15-xcode16.0
45

56
HELLO_SRC = hello.c
67
LIBHELLO_SRC = libhello.c
@@ -42,13 +43,24 @@ else ifeq ($(USE),10.6-xcode3.2.6)
4243
USE_DIRS := i386 x86_64 ppc fat-i386-x86_64 fat-i386-ppc
4344
NO_UPWARD := 1
4445
NO_LAZY := 1
46+
NO_DELAY_INIT := 1
4547
else ifeq ($(USE),10.11-xcode7.3)
4648
USE_DIRS := i386 x86_64 fat-i386-x86_64
49+
NO_DELAY_INIT := 1
50+
else ifeq ($(USE),15-xcode16.0)
51+
USE_DIRS := x86_64
52+
NO_LAZY := 1
4753
else
4854
# Warn about unspecified subset, but effectively fall back to 10.11-xcode7.3.
4955
$(warning USE - Option either unset or invalid. Using a safe fallback.)
50-
$(warning USE - Valid choices: all, 10.6-xcode3.2.6, 10.11-xcode7.3.)
56+
$(warning USE - Valid choices: all, 10.6-xcode3.2.6, 10.11-xcode7.3, 15-xcode16.0.)
5157
USE_DIRS := i386 x86_64 fat-i386-x86_64
58+
NO_DELAY_INIT := 1
59+
NO_LAZY := 1
60+
endif
61+
62+
ifeq ($(NO_DELAY_INIT),)
63+
TARGET_FILES += dylib_use_command-weak-delay.bin
5264
endif
5365

5466
# Setup target names from all/used architecture directories.
@@ -84,10 +96,10 @@ $(ALL_DIRS):
8496

8597
# Setup architecture-specific per-file targets (`<arch>/<file>`).
8698
%/hello.o: $(HELLO_SRC) %
87-
$(CC) $(ARCH_FLAGS) -o $@ -c $<
99+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -c $<
88100

89101
%/hello.bin: $(HELLO_SRC) %
90-
$(CC) $(ARCH_FLAGS) -o $@ $(RPATH_FLAGS) $<
102+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ $(RPATH_FLAGS) $<
91103

92104
%/hello_expected.bin: %/hello.bin
93105
cp $< $@
@@ -97,18 +109,21 @@ $(ALL_DIRS):
97109
cp $< $@
98110
install_name_tool -rpath made_up_path /usr/lib $@
99111

112+
%/dylib_use_command-weak-delay.bin: $(HELLO_SRC) %
113+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -Wl,-weak-l,z -Wl,-delay-l,z $<
114+
100115
%/libhello.dylib: $(LIBHELLO_SRC) %
101-
$(CC) $(ARCH_FLAGS) -o $@ -dynamiclib $<
116+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $<
102117

103118
%/libhello_expected.dylib: %/libhello.dylib
104119
cp $< $@
105120
install_name_tool -id test $@
106121

107122
%/libextrahello.dylib: $(LIBHELLO_SRC) % %/libhello.dylib
108-
$(CC) $(ARCH_FLAGS) -o $@ -dynamiclib $< $(LIBEXTRA_LDADD)
123+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -o $@ -dynamiclib $< $(LIBEXTRA_LDADD)
109124

110125
%/hellobundle.so: $(LIBHELLO_SRC) %
111-
$(CC) $(ARCH_FLAGS) -bundle $< -o $@
126+
$(CC) $(CFLAGS) $(ARCH_FLAGS) -bundle $< -o $@
112127

113128
# build inconsistent binaries
114129
.PHONY: inconsistent

test/test_create_load_commands.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_create_dylib_commands
2424
lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", 0, 0, 0)
2525

2626
assert lc
27-
assert_kind_of MachO::LoadCommands::DylibCommand, lc
27+
assert_instance_of MachO::LoadCommands::DylibCommand, lc
2828
assert lc.name
2929
assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name
3030
assert_equal "test", lc.name.to_s
@@ -36,6 +36,26 @@ def test_create_dylib_commands
3636
end
3737
end
3838

39+
def test_create_dylib_commands_new
40+
# all dylib commands are creatable, so test them all
41+
dylib_commands = %i[LC_LOAD_DYLIB LC_LOAD_WEAK_DYLIB]
42+
dylib_commands.each do |cmd_sym|
43+
lc = MachO::LoadCommands::LoadCommand.create(cmd_sym, "test", MachO::LoadCommands::DYLIB_USE_MARKER, 0, 0, 0)
44+
45+
assert lc
46+
assert_instance_of MachO::LoadCommands::DylibUseCommand, lc
47+
assert lc.name
48+
assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, lc.name
49+
assert_equal "test", lc.name.to_s
50+
assert_equal lc.name.to_s, lc.to_s
51+
assert_equal MachO::LoadCommands::DYLIB_USE_MARKER, lc.timestamp
52+
assert_equal 0, lc.current_version
53+
assert_equal 0, lc.compatibility_version
54+
assert_equal 0, lc.flags
55+
assert_instance_of String, lc.view.inspect
56+
end
57+
end
58+
3959
def test_create_rpath_command
4060
lc = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, "test")
4161

test/test_macho.rb

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,20 @@ def test_dylib
235235

236236
def test_extra_dylib
237237
filenames = SINGLE_ARCHES.map { |a| fixture(a, "libextrahello.dylib") }
238-
unusual_dylib_lcs = %i[
239-
LC_LOAD_UPWARD_DYLIB
240-
LC_LAZY_LOAD_DYLIB
241-
LC_LOAD_WEAK_DYLIB
242-
LC_REEXPORT_DYLIB
243-
]
238+
unusual_dylib_lcs = {
239+
LC_LOAD_UPWARD_DYLIB: :DYLIB_USE_UPWARD,
240+
LC_LAZY_LOAD_DYLIB: nil,
241+
LC_LOAD_WEAK_DYLIB: :DYLIB_USE_WEAK_LINK,
242+
LC_REEXPORT_DYLIB: :DYLIB_USE_REEXPORT,
243+
}
244244

245245
filenames.each do |fn|
246246
file = MachO::MachOFile.new(fn)
247247

248248
assert file.dylib?
249249

250250
# make sure we can read more unusual dylib load commands
251-
unusual_dylib_lcs.each do |cmdname|
251+
unusual_dylib_lcs.each do |cmdname, flag_name|
252252
lc = file[cmdname].first
253253

254254
# PPC and x86-family binaries don't have the same dylib LCs, so ignore
@@ -262,10 +262,37 @@ def test_extra_dylib
262262

263263
assert dylib_name
264264
assert_kind_of MachO::LoadCommands::LoadCommand::LCStr, dylib_name
265+
266+
assert lc.flag?(flag_name) if flag_name
267+
(unusual_dylib_lcs.values - [flag_name]).compact.each do |other_flag_name|
268+
refute lc.flag?(other_flag_name)
269+
end
265270
end
266271
end
267272
end
268273

274+
def test_dylib_use_command
275+
filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") }
276+
277+
filenames.each do |fn|
278+
file = MachO::MachOFile.new(fn)
279+
280+
lc = file[:LC_LOAD_WEAK_DYLIB].first
281+
lc2 = file[:LC_LOAD_DYLIB].first
282+
283+
assert_instance_of MachO::LoadCommands::DylibUseCommand, lc
284+
assert_instance_of MachO::LoadCommands::DylibCommand, lc2
285+
286+
refute_equal lc.flags, 0
287+
288+
assert lc.flag?(:DYLIB_USE_WEAK_LINK)
289+
assert lc.flag?(:DYLIB_USE_DELAYED_INIT)
290+
refute lc.flag?(:DYLIB_USE_UPWARD)
291+
292+
refute lc2.flag?(:DYLIB_USE_WEAK_LINK)
293+
end
294+
end
295+
269296
def test_bundle
270297
filenames = SINGLE_ARCHES.map { |a| fixture(a, "hellobundle.so") }
271298

test/test_serialize_load_commands.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ def test_serialize_load_dylib
6969
lc.compatibility_version)
7070
blob = lc.view.raw_data[lc.view.offset, lc.cmdsize]
7171

72+
assert_instance_of lc.class, lc2
73+
assert_equal blob, lc.serialize(ctx)
74+
assert_equal blob, lc2.serialize(ctx)
75+
end
76+
end
77+
78+
def test_serialize_load_dylib_new
79+
filenames = SINGLE_64_ARCHES.map { |a| fixture(a, "dylib_use_command-weak-delay.bin") }
80+
81+
filenames.each do |filename|
82+
file = MachO::MachOFile.new(filename)
83+
ctx = MachO::LoadCommands::LoadCommand::SerializationContext.context_for(file)
84+
lc = file[:LC_LOAD_WEAK_DYLIB].first
85+
lc2 = MachO::LoadCommands::LoadCommand.create(:LC_LOAD_WEAK_DYLIB, lc.name.to_s,
86+
lc.marker, lc.current_version,
87+
lc.compatibility_version, lc.flags)
88+
blob = lc.view.raw_data[lc.view.offset, lc.cmdsize]
89+
90+
assert_instance_of lc.class, lc2
7291
assert_equal blob, lc.serialize(ctx)
7392
assert_equal blob, lc2.serialize(ctx)
7493
end

0 commit comments

Comments
 (0)