Skip to content

Commit e62d222

Browse files
committed
macho/load_commands: support new macOS 15 dylib use command
1 parent a3fc5a5 commit e62d222

File tree

8 files changed

+200
-18
lines changed

8 files changed

+200
-18
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: 101 additions & 3 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
@@ -228,11 +242,19 @@ def self.create(cmd_sym, *args)
228242
raise LoadCommandNotCreatableError, cmd_sym unless CREATABLE_LOAD_COMMANDS.include?(cmd_sym)
229243

230244
klass = LoadCommands.const_get LC_STRUCTURES[cmd_sym]
231-
cmd = LOAD_COMMAND_CONSTANTS[cmd_sym]
232245

233246
# cmd will be filled in, view and cmdsize will be left unpopulated
234247
klass_arity = klass.min_args - 3
235248

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

238260
klass.new(nil, cmd, nil, *args)
@@ -528,6 +550,23 @@ class DylibCommand < LoadCommand
528550
# @return [Integer] the library's compatibility version number
529551
field :compatibility_version, :uint32
530552

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

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

test/helpers.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module Helpers
2626

2727
# architectures used in testing 64-bit single-arch binaries
2828
SINGLE_64_ARCHES = [
29-
:x86_64,
29+
:x86_64
3030
].freeze
3131

3232
# architectures used in testing single-arch binaries

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(:arm64, "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)